Compare commits

..

40 Commits

Author SHA1 Message Date
hsc
8daa1df8e9 串口连接修复 2026-08-03 13:07:03 +08:00
hsc
7776e6c7ab 加载配置文件逻辑优化 2026-07-30 13:23:59 +08:00
hsc
2e71f68428 Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/ADP 2026-07-29 10:31:20 +08:00
hsc
388edd1ada 公共变量编辑限制 2026-07-29 10:31:10 +08:00
6057a71315 数据报表导出逻辑改为导出选中行数据 2026-07-29 09:58:23 +08:00
hsc
12de1b61e0 Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/ADP 2026-07-28 14:41:59 +08:00
hsc
95700bfb8c CAN驱动还原(未做修改) 2026-07-28 14:41:46 +08:00
hsc
013729003a 曲线回读代码位置修改 2026-07-28 14:35:33 +08:00
hsc
add1cd95be 曲线回读代码位置修改 2026-07-27 14:18:12 +08:00
hsc
2a13c0ad34 Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/ADP 2026-07-27 13:21:19 +08:00
hsc
92d477972f 曲线回读 2026-07-27 13:21:04 +08:00
14d818083d 完善数据报表模块 2026-07-27 13:19:06 +08:00
hsc
07218fc737 曲线回读+测试报告导出前期工作 2026-07-27 10:19:55 +08:00
hsc
c592bcd272 测试报告导出 2026-07-23 10:52:49 +08:00
hsc
092087f670 异常处理停止正常程序 2026-07-23 10:14:39 +08:00
hsc
9281b48a2b Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/ADP 2026-07-23 09:18:04 +08:00
hsc
a9b6bc245d 曲线回读 2026-07-23 09:01:44 +08:00
hsc
2cd8ef61db 曲线回读 2026-07-22 17:08:36 +08:00
hsc
8b8704e59c Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/ADP 2026-07-22 16:49:22 +08:00
hsc
54db9ac0b4 小bug修复 2026-07-22 16:47:57 +08:00
hsc
ab7936c4ed 加密狗添加,io板卡映射修改 2026-07-22 16:36:15 +08:00
hsc
be7b459385 上机测试优化2 2026-07-21 16:53:23 +08:00
hsc
4386449925 上机bug修复,命令修复添加 2026-07-21 14:36:15 +08:00
hsc
9661e5f063 异常处理 2026-07-20 18:20:13 +08:00
hsc
b79eea918e 台架程序tp化 2026-07-20 17:43:18 +08:00
hsc
7f2721ab11 io板卡兼容优化 2026-07-20 16:33:09 +08:00
hsc
48492973c3 弹性连接设备添加 2026-07-20 13:33:27 +08:00
hsc
2a363d37cd 屏蔽n36200设备,屏蔽第九台架 2026-07-20 13:29:21 +08:00
hsc
32d62de6f8 台架驱动 2026-07-20 13:20:28 +08:00
hsc
c0a24af4cb 台架驱动 2026-07-17 17:36:08 +08:00
hsc
c4ec348217 图表x轴显示优化 2026-07-17 09:51:11 +08:00
hsc
74d544c288 canbug修复 2026-07-17 09:42:50 +08:00
hsc
2dfc819bcc SDS2000X示波器设备调试 2026-07-16 16:53:37 +08:00
hsc
0ed080f63c N36600修改 2026-07-16 10:35:32 +08:00
hsc
8e44be4894 CAN优化 2026-07-15 15:47:42 +08:00
hsc
aebbd88e23 CAN适配兼容 2026-07-14 16:50:57 +08:00
hsc
2fe307c142 spaw7000设备命令修改 2026-07-14 14:13:39 +08:00
hsc
3318c720b7 IO板卡系统优化 2026-07-13 15:59:02 +08:00
hsc
b6b767cf0a 解决stepsmanager bug 2026-07-13 14:06:37 +08:00
hsc
9171e0b0c1 自定义报文发送 2026-07-13 13:23:27 +08:00
96 changed files with 5812 additions and 1310 deletions

20
ADP.sln
View File

@@ -43,6 +43,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchMovementModule", "Benc
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CANModule", "CANModule\CANModule.csproj", "{AF2533DD-599D-49D7-942B-B0C26534D15F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CANModule", "CANModule\CANModule.csproj", "{AF2533DD-599D-49D7-942B-B0C26534D15F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "加密狗", "加密狗\加密狗.csproj", "{A35CA316-7B5B-4183-8636-4ECA532B1489}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurveModule", "CurveModule\CurveModule.csproj", "{84006890-9288-44C4-9D17-FA88F3FDECFA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportModule", "ExportModule\ExportModule.csproj", "{8923122F-F7D3-4AE7-9E86-9E78F1296A11}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -125,6 +131,18 @@ Global
{AF2533DD-599D-49D7-942B-B0C26534D15F}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF2533DD-599D-49D7-942B-B0C26534D15F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AF2533DD-599D-49D7-942B-B0C26534D15F}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF2533DD-599D-49D7-942B-B0C26534D15F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AF2533DD-599D-49D7-942B-B0C26534D15F}.Release|Any CPU.Build.0 = Release|Any CPU {AF2533DD-599D-49D7-942B-B0C26534D15F}.Release|Any CPU.Build.0 = Release|Any CPU
{A35CA316-7B5B-4183-8636-4ECA532B1489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A35CA316-7B5B-4183-8636-4ECA532B1489}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A35CA316-7B5B-4183-8636-4ECA532B1489}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A35CA316-7B5B-4183-8636-4ECA532B1489}.Release|Any CPU.Build.0 = Release|Any CPU
{84006890-9288-44C4-9D17-FA88F3FDECFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84006890-9288-44C4-9D17-FA88F3FDECFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84006890-9288-44C4-9D17-FA88F3FDECFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{84006890-9288-44C4-9D17-FA88F3FDECFA}.Release|Any CPU.Build.0 = Release|Any CPU
{8923122F-F7D3-4AE7-9E86-9E78F1296A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8923122F-F7D3-4AE7-9E86-9E78F1296A11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8923122F-F7D3-4AE7-9E86-9E78F1296A11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8923122F-F7D3-4AE7-9E86-9E78F1296A11}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -139,5 +157,7 @@ Global
{170AD4C1-189D-4FBE-B10D-2A4304527834} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {170AD4C1-189D-4FBE-B10D-2A4304527834} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{31F84D0E-3634-4C70-A830-21AF412E3BBF} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {31F84D0E-3634-4C70-A830-21AF412E3BBF} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -22,8 +22,10 @@
<ProjectReference Include="..\BenchMovementModule\BenchMovementModule.csproj" /> <ProjectReference Include="..\BenchMovementModule\BenchMovementModule.csproj" />
<ProjectReference Include="..\CANModule\CANModule.csproj" /> <ProjectReference Include="..\CANModule\CANModule.csproj" />
<ProjectReference Include="..\Command\Command.csproj" /> <ProjectReference Include="..\Command\Command.csproj" />
<ProjectReference Include="..\CurveModule\CurveModule.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" /> <ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\DeviceEditModule\DeviceEditModule.csproj" /> <ProjectReference Include="..\DeviceEditModule\DeviceEditModule.csproj" />
<ProjectReference Include="..\ExportModule\ExportModule.csproj" />
<ProjectReference Include="..\LoginModule\LoginModule.csproj" /> <ProjectReference Include="..\LoginModule\LoginModule.csproj" />
<ProjectReference Include="..\MainModule\MainModule.csproj" /> <ProjectReference Include="..\MainModule\MainModule.csproj" />
<ProjectReference Include="..\MonitorModule\MonitorModule.csproj" /> <ProjectReference Include="..\MonitorModule\MonitorModule.csproj" />
@@ -33,6 +35,7 @@
<ProjectReference Include="..\UIShare\UIShare.csproj" /> <ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" /> <ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" /> <ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
<ProjectReference Include="..\加密狗\加密狗.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -18,11 +18,12 @@ using static System.Runtime.InteropServices.JavaScript.JSType;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare; using UIShare;
using System; using System;
using DeviceCommand.Device; using DeviceCommand.Devices;
using DeviceCommand.Base; using DeviceCommand.Base;
using AutoMapper; using AutoMapper;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using ADP.Profiles; using ADP.Profiles;
using ;
namespace ADP namespace ADP
{ {
@@ -75,6 +76,52 @@ namespace ADP
RegionManager.SetRegionManager(Application.Current.MainWindow, re); RegionManager.SetRegionManager(Application.Current.MainWindow, re);
login.Show(); login.Show();
} }
protected override void OnStartup(StartupEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
base.OnStartup(e);
return;
}
string strUserSn = "ACA89B2C1194650D4E93D97BE189FFCC";
string strApiKey = "C6D01BCD442347822876F8232245DE9422643C167B8C61CE4DF6049990E7395588675A8FC6AA4BF7AD353D669C993236BEA83F77551753885A47AF34185CC3A7";
Byte[] buffer = new Byte[256];
buffer = System.Text.Encoding.Default.GetBytes(strUserSn);
uint retcode;
retcode = .SN(buffer);
if (retcode != 0)
{
MessageBox.Show(string.Format("设置UserSN失败 error code: 0x{0:x}", retcode));
return;
}
uint count = 0;
retcode = API类.VikeyFind(ref count);
if (retcode != 0 || count == 0)
{
MessageBox.Show("未找到加密狗");
return;
}
buffer = System.Text.Encoding.Default.GetBytes(strApiKey);
retcode = .APIkey(0, buffer);
if (retcode != 0)
{
MessageBox.Show(string.Format("设置api密钥失败 error code: 0x{0:x}", retcode));
return;
}
var remaining = .(0);
if (remaining <= TimeSpan.Zero)
{
MessageBox.Show("加密狗已到期或未能读取时间,请联系供应商。", "加密狗验证失败", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
base.OnStartup(e);
}
protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry) protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
{ {
base.RegisterRequiredTypes(containerRegistry); base.RegisterRequiredTypes(containerRegistry);
@@ -104,6 +151,7 @@ namespace ADP
containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>)); containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
//注册服务 //注册服务
containerRegistry.Register<IMonitorValueService, MonitorValueService>(); containerRegistry.Register<IMonitorValueService, MonitorValueService>();
containerRegistry.Register<ITestReportService, TestReportService>();
} }
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中) //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
protected override IModuleCatalog CreateModuleCatalog() protected override IModuleCatalog CreateModuleCatalog()

View File

@@ -1,6 +1,7 @@
using UIShare.PubEvent; using UIShare.PubEvent;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
namespace ADP.ViewModels.Dialogs namespace ADP.ViewModels.Dialogs
{ {
@@ -22,8 +23,8 @@ namespace ADP.ViewModels.Dialogs
set => SetProperty(ref _Message, value); set => SetProperty(ref _Message, value);
} }
private string _Icon= $"pack://siteoforigin:,,,/Resources/Images/info.png"; private ImageSource _Icon = new System.Windows.Media.Imaging.BitmapImage(new System.Uri("pack://application:,,,/Images/info.png"));
public string Icon public ImageSource Icon
{ {
get => _Icon; get => _Icon;
set => SetProperty(ref _Icon, value); set => SetProperty(ref _Icon, value);
@@ -101,13 +102,15 @@ namespace ADP.ViewModels.Dialogs
Title = parameters.GetValue<string>("Title"); Title = parameters.GetValue<string>("Title");
Message = parameters.GetValue<string>("Message"); Message = parameters.GetValue<string>("Message");
var iconKey = parameters.GetValue<string>("Icon"); // info / error / warn var iconKey = parameters.GetValue<string>("Icon"); // info / error / warn
Icon = iconKey switch var bmp = (ImageSource)new System.Windows.Media.Imaging.BitmapImage(new System.Uri(iconKey switch
{ {
"info" => $"pack://siteoforigin:,,,/Resources/Images/info.png", "info" => "pack://application:,,,/Images/info.png",
"error" => $"pack://siteoforigin:,,,/Resources/Images/error.png", "error" => "pack://application:,,,/Images/error.png",
"warn" => $"pack://siteoforigin:,,,/Resources/Images/warning.png", "warn" => "pack://application:,,,/Images/warning.png",
_ => $"pack://siteoforigin:,,,/Resources/Images/info.png" // 默认 _ => "pack://application:,,,/Images/info.png"
}; }));
bmp.Freeze();
Icon = bmp;
ShowYes = parameters.GetValue<bool>("ShowYes"); ShowYes = parameters.GetValue<bool>("ShowYes");

View File

@@ -1,4 +1,5 @@
using DeviceCommand.Devices;
using Logger; using Logger;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Microsoft.Win32; using Microsoft.Win32;
@@ -11,12 +12,12 @@ using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; // 💡 新增:引入定时器命名空间 using System.Windows.Threading;
using UIShare; using UIShare;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare.PubEvent; using UIShare.PubEvent;
using UIShare.UIViewModel; using UIShare.UIViewModel;
using ZLGUSBCANFD; using SqlSugar;
namespace ADP.ViewModels namespace ADP.ViewModels
{ {
@@ -152,6 +153,7 @@ namespace ADP.ViewModels
public ICommand SelectCANMessageCommand { get; set; } public ICommand SelectCANMessageCommand { get; set; }
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; }
#endregion #endregion
public ShellViewModel(IContainerProvider containerProvider) public ShellViewModel(IContainerProvider containerProvider)
@@ -185,6 +187,7 @@ namespace ADP.ViewModels
SelectCANMessageCommand = new DelegateCommand(SelectCANMessage); SelectCANMessageCommand = new DelegateCommand(SelectCANMessage);
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting); MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
GetFileStringCommand = new DelegateCommand(GetFileString); GetFileStringCommand = new DelegateCommand(GetFileString);
SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
_globalInfo.ContextDic.Add("default", new ScopedContext()); _globalInfo.ContextDic.Add("default", new ScopedContext());
@@ -218,6 +221,12 @@ namespace ADP.ViewModels
} }
#region #region
private async Task SilenceBuzzer()
{
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Publish(_globalInfo.CurrentScope);
}
private void GetFileString() private void GetFileString()
{ {
var openFileDialog = new OpenFileDialog var openFileDialog = new OpenFileDialog
@@ -558,7 +567,7 @@ namespace ADP.ViewModels
ScopedContext? targetContext = CurrentContext; ScopedContext? targetContext = CurrentContext;
if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return; if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
CurrentConfig.CurrentADPFile = "新建ADP文件";
// 💡 2. 严格对快照隔离的 Context 数据进行清理,不影响其他工位 // 💡 2. 严格对快照隔离的 Context 数据进行清理,不影响其他工位
targetContext.CurrentFilePath = null; targetContext.CurrentFilePath = null;
targetContext.Program.Parameters.Clear(); targetContext.Program.Parameters.Clear();
@@ -622,7 +631,7 @@ namespace ADP.ViewModels
LoggerHelper.WarnWithNotify(runningScope, $"文件格式不正确或为空: {filePath}"); LoggerHelper.WarnWithNotify(runningScope, $"文件格式不正确或为空: {filePath}");
return; return;
} }
CurrentConfig.CurrentADPFile = filePath;
// 💡 2. 严格赋值给快照锁定下的当前工位上下文,实现数据完全隔离 // 💡 2. 严格赋值给快照锁定下的当前工位上下文,实现数据完全隔离
targetContext.Program.Parameters = program.Parameters; targetContext.Program.Parameters = program.Parameters;
targetContext.Program.StepCollection = program.StepCollection; targetContext.Program.StepCollection = program.StepCollection;
@@ -682,6 +691,7 @@ namespace ADP.ViewModels
if (saveFileDialog.ShowDialog() == true) if (saveFileDialog.ShowDialog() == true)
{ {
// 💡 2. 将新路径安全写入快照工位 // 💡 2. 将新路径安全写入快照工位
CurrentConfig.CurrentADPFile = saveFileDialog.FileName;
targetContext.CurrentFilePath = saveFileDialog.FileName; targetContext.CurrentFilePath = saveFileDialog.FileName;
// 💡 3. 调用你的底层文件保存逻辑,传入快照工位的 Program 模型 // 💡 3. 调用你的底层文件保存逻辑,传入快照工位的 Program 模型
@@ -835,6 +845,14 @@ namespace ADP.ViewModels
if (_globalInfo.CurrentScope != "default") return; if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "BenchMovementView"); _regionManager.RequestNavigate("ShellViewManager", "BenchMovementView");
break; break;
case "曲线回读界面":
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "CurveRecallView");
break;
case "测试报告导出界面":
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "ExportView");
break;
} }
} }

View File

@@ -66,6 +66,16 @@
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}" Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" /> Margin="8" />
<Button Content="曲线回读界面"
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
<Button Content="测试报告导出界面"
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
</StackPanel> </StackPanel>
</materialDesign:DrawerHost.LeftDrawerContent> </materialDesign:DrawerHost.LeftDrawerContent>
<Grid> <Grid>
@@ -184,6 +194,7 @@ Command="{Binding SelectCANMessageCommand}">
Foreground="Black" /> Foreground="Black" />
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<!-- 获得文件路径字符串 --> <!-- 获得文件路径字符串 -->
<MenuItem Header="获得文件路径字符串" <MenuItem Header="获得文件路径字符串"
Foreground="Black" Foreground="Black"
@@ -194,7 +205,7 @@ Command="{Binding GetFileStringCommand}">
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<!-- 蜂鸣器消音 --> <!-- 弹窗管理器 -->
<MenuItem Header="弹窗管理器" <MenuItem Header="弹窗管理器"
FontSize="14" FontSize="14"
Height="50" Height="50"

View File

@@ -0,0 +1,164 @@
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
}
}

View File

@@ -0,0 +1,28 @@
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);
}
}
}

View File

@@ -0,0 +1,27 @@
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);
}
}
}

View File

@@ -1,411 +1,520 @@
using System; using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
using DeviceCommand.Devices;
using Logger;
using Prism.Ioc;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
namespace BenchMovementModule.ViewModels namespace BenchMovementModule.ViewModels
{ {
public class BenchMovementViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable public class BenchMovementViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{ {
#region #region
public bool KeepAlive => true; public bool KeepAlive => true;
// --- 1. 台架位置区 --- // ===== GantryControlTcp 实例(从 DeviceManager 获取)=====
private double _posX; private GantryControlTcp? _gantry;
public double PosX private DeviceManager? _deviceManager;
private readonly GlobalInfo _globalInfo;
// ===== 连接状态 =====
private bool _isConnected;
public bool IsConnected
{ {
get => _posX; get => _isConnected;
set => SetProperty(ref _posX, value); set => SetProperty(ref _isConnected, value);
} }
private double _posY; private string _connectionStatus = "未连接";
public double PosY public string ConnectionStatus
{ {
get => _posY; get => _connectionStatus;
set => SetProperty(ref _posY, value); set => SetProperty(ref _connectionStatus, value);
} }
private double _posZ; // ===== 6轴绝对位置显示相对原点的实时位置只读=====
public double PosZ private int _absPos1;
{ public int AbsPos1 { get => _absPos1; set => SetProperty(ref _absPos1, value); }
get => _posZ;
set => SetProperty(ref _posZ, value);
}
// --- 2. 信号回读区 --- private int _absPos2;
private double _pos1X; public int AbsPos2 { get => _absPos2; set => SetProperty(ref _absPos2, value); }
public double Pos1X
{
get => _pos1X;
set => SetProperty(ref _pos1X, value);
}
private int _pos1Quality; private int _absPos3;
public int Pos1Quality public int AbsPos3 { get => _absPos3; set => SetProperty(ref _absPos3, value); }
{
get => _pos1Quality;
set => SetProperty(ref _pos1Quality, value);
}
private double _pos2X; private int _absPos4;
public double Pos2X public int AbsPos4 { get => _absPos4; set => SetProperty(ref _absPos4, value); }
{
get => _pos2X;
set => SetProperty(ref _pos2X, value);
}
private int _pos2Quality; private int _absPos5;
public int Pos2Quality public int AbsPos5 { get => _absPos5; set => SetProperty(ref _absPos5, value); }
{
get => _pos2Quality;
set => SetProperty(ref _pos2Quality, value);
}
private double _pos1Y; private int _absPos6;
public double Pos1Y public int AbsPos6 { get => _absPos6; set => SetProperty(ref _absPos6, value); }
{
get => _pos1Y;
set => SetProperty(ref _pos1Y, value);
}
private double _pos2Y; // ===== 寸动目标位置 =====
public double Pos2Y private int _targetX;
{ public int TargetX { get => _targetX; set => SetProperty(ref _targetX, value); }
get => _pos2Y;
set => SetProperty(ref _pos2Y, value);
}
// --- 3. 扫描参数区 --- private int _targetY;
private double _scanStepX = 10; public int TargetY { get => _targetY; set => SetProperty(ref _targetY, value); }
public double ScanStepX
{
get => _scanStepX;
set => SetProperty(ref _scanStepX, value);
}
private double _scanStartX = -120; private int _targetZ;
public double ScanStartX public int TargetZ { get => _targetZ; set => SetProperty(ref _targetZ, value); }
{
get => _scanStartX;
set => SetProperty(ref _scanStartX, value);
}
private double _scanEndX = 120; // ===== 扫描参数 =====
public double ScanEndX private int _scanStepX = 1000;
{ public int ScanStepX { get => _scanStepX; set => SetProperty(ref _scanStepX, value); }
get => _scanEndX;
set => SetProperty(ref _scanEndX, value);
}
private double _scanStepY = 10; private int _scanStartX;
public double ScanStepY public int ScanStartX { get => _scanStartX; set => SetProperty(ref _scanStartX, value); }
{
get => _scanStepY;
set => SetProperty(ref _scanStepY, value);
}
private double _scanStartY = -120; private int _scanEndX = 50000;
public double ScanStartY public int ScanEndX { get => _scanEndX; set => SetProperty(ref _scanEndX, value); }
{
get => _scanStartY;
set => SetProperty(ref _scanStartY, value);
}
private double _scanEndY = 120; private int _scanStepY = 1000;
public double ScanEndY public int ScanStepY { get => _scanStepY; set => SetProperty(ref _scanStepY, value); }
{
get => _scanEndY;
set => SetProperty(ref _scanEndY, value);
}
private double _scanStepZ = 1; private int _scanStartY;
public double ScanStepZ public int ScanStartY { get => _scanStartY; set => SetProperty(ref _scanStartY, value); }
{
get => _scanStepZ;
set => SetProperty(ref _scanStepZ, value);
}
private double _scanStartZ = 0; private int _scanEndY = 50000;
public double ScanStartZ public int ScanEndY { get => _scanEndY; set => SetProperty(ref _scanEndY, value); }
{
get => _scanStartZ;
set => SetProperty(ref _scanStartZ, value);
}
private double _scanEndZ = 0; private int _scanStepZ = 100;
public double ScanEndZ public int ScanStepZ { get => _scanStepZ; set => SetProperty(ref _scanStepZ, value); }
{
get => _scanEndZ;
set => SetProperty(ref _scanEndZ, value);
}
// --- 4. 寸动参数区 --- private int _scanStartZ;
private double _jogX; public int ScanStartZ { get => _scanStartZ; set => SetProperty(ref _scanStartZ, value); }
public double JogX
{
get => _jogX;
set => SetProperty(ref _jogX, value);
}
private double _jogY; private int _scanEndZ = 20000;
public double JogY public int ScanEndZ { get => _scanEndZ; set => SetProperty(ref _scanEndZ, value); }
{
get => _jogY;
set => SetProperty(ref _jogY, value);
}
private double _jogZ; // ===== 速度设定(脉冲)=====
public double JogZ private int _speedX = 5000;
{ public int SpeedX { get => _speedX; set => SetProperty(ref _speedX, value); }
get => _jogZ;
set => SetProperty(ref _jogZ, value);
}
// --- 5. 设置页网络及PLC参数 --- private int _speedY = 5000;
private string _plcIp = "192.168.0.30"; public int SpeedY { get => _speedY; set => SetProperty(ref _speedY, value); }
public string PlcIp
{
get => _plcIp;
set => SetProperty(ref _plcIp, value);
}
private int _plcPort = 502; private int _speedZ = 2000;
public int PlcPort public int SpeedZ { get => _speedZ; set => SetProperty(ref _speedZ, value); }
{
get => _plcPort;
set => SetProperty(ref _plcPort, value);
}
// --- 6. 设置页:速度与零点设定 --- private int _homeSpeed = 3000;
private double _speedX; public int HomeSpeed { get => _homeSpeed; set => SetProperty(ref _homeSpeed, value); }
public double SpeedX
{
get => _speedX;
set => SetProperty(ref _speedX, value);
}
private double _speedY; // ===== 零点设定 =====
public double SpeedY private int _homeX;
{ public int HomeX { get => _homeX; set => SetProperty(ref _homeX, value); }
get => _speedY;
set => SetProperty(ref _speedY, value);
}
private double _speedZ; private int _homeY;
public double SpeedZ public int HomeY { get => _homeY; set => SetProperty(ref _homeY, value); }
{
get => _speedZ;
set => SetProperty(ref _speedZ, value);
}
private double _zeroX; private int _homeZ;
public double ZeroX public int HomeZ { get => _homeZ; set => SetProperty(ref _homeZ, value); }
{
get => _zeroX;
set => SetProperty(ref _zeroX, value);
}
private double _zeroY;
public double ZeroY
{
get => _zeroY;
set => SetProperty(ref _zeroY, value);
}
private double _zeroZ;
public double ZeroZ
{
get => _zeroZ;
set => SetProperty(ref _zeroZ, value);
}
// --- 7. 设置页CAN设定信号映射 ---
private int _canChannel = 0;
public int CanChannel
{
get => _canChannel;
set => SetProperty(ref _canChannel, value);
}
private string _canMsgId = "0X0";
public string CanMsgId
{
get => _canMsgId;
set => SetProperty(ref _canMsgId, value);
}
private string _canMsgName = string.Empty;
public string CanMsgName
{
get => _canMsgName;
set => SetProperty(ref _canMsgName, value);
}
private string _sigPos1X = string.Empty;
public string SigPos1X
{
get => _sigPos1X;
set => SetProperty(ref _sigPos1X, value);
}
private string _sigPos2X = string.Empty;
public string SigPos2X
{
get => _sigPos2X;
set => SetProperty(ref _sigPos2X, value);
}
private string _sigPos1Y = string.Empty;
public string SigPos1Y
{
get => _sigPos1Y;
set => SetProperty(ref _sigPos1Y, value);
}
private string _sigPos2Y = string.Empty;
public string SigPos2Y
{
get => _sigPos2Y;
set => SetProperty(ref _sigPos2Y, value);
}
private string _sigPos1Quality = string.Empty;
public string SigPos1Quality
{
get => _sigPos1Quality;
set => SetProperty(ref _sigPos1Quality, value);
}
private string _sigPos2Quality = string.Empty;
public string SigPos2Quality
{
get => _sigPos2Quality;
set => SetProperty(ref _sigPos2Quality, value);
}
#endregion #endregion
#region #region
// 扫描控制面板命令 public ICommand ConnectCommand { get; }
public ICommand StartScanCommand { get; } public ICommand DisconnectCommand { get; }
public ICommand StopScanCommand { get; }
public ICommand GoHomeCommand { get; }
// 寸动执行单轴移动
public ICommand MoveXCommand { get; } public ICommand MoveXCommand { get; }
public ICommand MoveYCommand { get; } public ICommand MoveYCommand { get; }
public ICommand MoveZCommand { get; } public ICommand MoveZCommand { get; }
public ICommand MoveAllCommand { get; }
// 全局控制 public ICommand GoHomeCommand { get; }
public ICommand EmergencyStopCommand { get; } public ICommand EmergencyStopCommand { get; }
public ICommand StopXCommand { get; }
// 参数设置应用命令 public ICommand StopYCommand { get; }
public ICommand ApplyNetworkCommand { get; } public ICommand StopZCommand { get; }
public ICommand SetSpeedCommand { get; } public ICommand SetSpeedCommand { get; }
public ICommand SetZeroCommand { get; } public ICommand SetHomeCommand { get; }
public ICommand ReadStatusCommand { get; }
#endregion #endregion
#region #region
private readonly IContainerProvider _containerProvider; private readonly DispatcherTimer _pollTimer;
private bool _isInitiated = false; private bool _isInitiated = false;
private string _testStatus = string.Empty;
#endregion #endregion
public BenchMovementViewModel(IContainerProvider containerProvider) : base(containerProvider) public BenchMovementViewModel(IContainerProvider containerProvider) : base(containerProvider)
{ {
_containerProvider = containerProvider; _globalInfo = containerProvider.Resolve<GlobalInfo>();
ConnectCommand = new DelegateCommand(OnConnect);
// 初始化扫描控制命令绑定 DisconnectCommand = new DelegateCommand(OnDisconnect);
StartScanCommand = new DelegateCommand(OnStartScan);
StopScanCommand = new DelegateCommand(OnStopScan);
GoHomeCommand = new DelegateCommand(OnGoHome);
// 初始化寸动命令绑定
MoveXCommand = new DelegateCommand(OnMoveX); MoveXCommand = new DelegateCommand(OnMoveX);
MoveYCommand = new DelegateCommand(OnMoveY); MoveYCommand = new DelegateCommand(OnMoveY);
MoveZCommand = new DelegateCommand(OnMoveZ); MoveZCommand = new DelegateCommand(OnMoveZ);
MoveAllCommand = new DelegateCommand(OnMoveAll);
// 急停与设置保存 GoHomeCommand = new DelegateCommand(OnGoHome);
EmergencyStopCommand = new DelegateCommand(OnEmergencyStop); EmergencyStopCommand = new DelegateCommand(OnEmergencyStop);
ApplyNetworkCommand = new DelegateCommand(OnApplyNetwork); StopXCommand = new DelegateCommand(OnStopX);
StopYCommand = new DelegateCommand(OnStopY);
StopZCommand = new DelegateCommand(OnStopZ);
SetSpeedCommand = new DelegateCommand(OnSetSpeed); SetSpeedCommand = new DelegateCommand(OnSetSpeed);
SetZeroCommand = new DelegateCommand(OnSetZero); SetHomeCommand = new DelegateCommand(OnSetHome);
ReadStatusCommand = new DelegateCommand(OnReadStatus);
// 位置轮询定时器500ms
_pollTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromMilliseconds(500)
};
_pollTimer.Tick += OnPollTick;
} }
#region #region
private void OnStartScan() private async void OnConnect()
{ {
// TODO: 等协议到了处理开始扫描 if (_gantry == null) return;
try
{
ConnectionStatus = "连接中...";
bool ok = await _gantry.ConnectAsync();
if (ok)
{
IsConnected = true;
ConnectionStatus = "已连接";
_pollTimer.Start();
await ReadDeviceParametersAsync();
LoggerHelper.Info("[BenchMovement] 台架连接成功");
}
else
{
ConnectionStatus = "连接失败";
}
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 连接失败: {ex.Message}");
ConnectionStatus = $"连接失败: {ex.Message}";
}
UpdateConnectionStatus();
} }
private void OnStopScan() private void OnDisconnect()
{ {
// TODO: 等协议到了处理停止扫描 if (_gantry == null) return;
try
{
_pollTimer.Stop();
_gantry.Close();
IsConnected = false;
ConnectionStatus = "未连接";
LoggerHelper.Info("[BenchMovement] 台架已断开");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 断开失败: {ex.Message}");
}
UpdateConnectionStatus();
} }
private void OnGoHome() private async void OnMoveX()
{ {
// TODO: 等协议到了处理台架回原点逻辑 if (!EnsureConnected()) return;
try
{
await _gantry!.SetAxisTargetPositionAsync(1, TargetX);
await _gantry.StartSingleAxisAsync(1);
LoggerHelper.Info($"[BenchMovement] X轴移动到 {TargetX}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] X轴移动失败: {ex.Message}");
ShowErrorMessageBox($"X轴移动失败: {ex.Message}", () => { });
}
} }
private void OnMoveX() private async void OnMoveY()
{ {
// TODO: 执行单轴X向绝对位置寸动 if (!EnsureConnected()) return;
try
{
await _gantry!.SetAxisTargetPositionAsync(2, TargetY);
await _gantry.StartSingleAxisAsync(2);
LoggerHelper.Info($"[BenchMovement] Y轴移动到 {TargetY}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] Y轴移动失败: {ex.Message}");
ShowErrorMessageBox($"Y轴移动失败: {ex.Message}", () => { });
}
} }
private void OnMoveY() private async void OnMoveZ()
{ {
// TODO: 执行单轴Y向绝对位置寸动 if (!EnsureConnected()) return;
try
{
await _gantry!.SetAxisTargetPositionAsync(3, TargetZ);
await _gantry.StartSingleAxisAsync(3);
LoggerHelper.Info($"[BenchMovement] Z轴移动到 {TargetZ}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] Z轴移动失败: {ex.Message}");
ShowErrorMessageBox($"Z轴移动失败: {ex.Message}", () => { });
}
} }
private void OnMoveZ() private async void OnMoveAll()
{ {
// TODO: 执行单轴Z向绝对位置寸动 if (!EnsureConnected()) return;
try
{
await _gantry!.SetAxisTargetPositionAsync(1, TargetX);
await _gantry.SetAxisTargetPositionAsync(2, TargetY);
await _gantry.SetAxisTargetPositionAsync(3, TargetZ);
await _gantry.StartAllAxesAsync();
LoggerHelper.Info($"[BenchMovement] 三轴同时移动: X={TargetX}, Y={TargetY}, Z={TargetZ}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 三轴移动失败: {ex.Message}");
ShowErrorMessageBox($"三轴移动失败: {ex.Message}", () => { });
}
} }
private void OnEmergencyStop() private async void OnGoHome()
{ {
// TODO: 全局急停高优先级报文/IO发送 if (!EnsureConnected()) return;
try
{
await _gantry!.HomeAsync();
LoggerHelper.Info("[BenchMovement] 回原点已触发");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 回原点失败: {ex.Message}");
ShowErrorMessageBox($"回原点失败: {ex.Message}", () => { });
}
} }
private void OnApplyNetwork() private async void OnEmergencyStop()
{ {
// TODO: 应用PLC的IP和端口网络连接配置 if (!EnsureConnected()) return;
try
{
await _gantry!.SetEStopStateAsync(true);
LoggerHelper.Info("[BenchMovement] 急停已触发");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 急停失败: {ex.Message}");
}
} }
private void OnSetSpeed() private async void OnStopX()
{ {
// TODO: 发送三轴移动速度设定参数 if (!EnsureConnected()) return;
try
{
await _gantry!.SetStopStateAsync(1, true);
await Task.Delay(100);
await _gantry.SetStopStateAsync(1, false);
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] X轴停止失败: {ex.Message}");
}
} }
private void OnSetZero() private async void OnStopY()
{ {
// TODO: 发送台架绝对位置零点校准设定 if (!EnsureConnected()) return;
try
{
await _gantry!.SetStopStateAsync(2, true);
await Task.Delay(100);
await _gantry.SetStopStateAsync(2, false);
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] Y轴停止失败: {ex.Message}");
}
}
private async void OnStopZ()
{
if (!EnsureConnected()) return;
try
{
await _gantry!.SetStopStateAsync(3, true);
await Task.Delay(100);
await _gantry.SetStopStateAsync(3, false);
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] Z轴停止失败: {ex.Message}");
}
}
private async void OnSetSpeed()
{
if (!EnsureConnected()) return;
try
{
await _gantry!.SetAxisSpeedAsync(1, SpeedX);
await _gantry.SetAxisSpeedAsync(2, SpeedY);
await _gantry.SetAxisSpeedAsync(3, SpeedZ);
await _gantry.SetHomePositionSpeedAsync(HomeSpeed);
LoggerHelper.Info($"[BenchMovement] 速度设定完成: X={SpeedX}, Y={SpeedY}, Z={SpeedZ}, 回零={HomeSpeed}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 速度设定失败: {ex.Message}");
ShowErrorMessageBox($"速度设定失败: {ex.Message}", () => { });
}
}
private async void OnSetHome()
{
if (!EnsureConnected()) return;
try
{
await _gantry!.SetHomePositionAsync(HomeX, HomeY, HomeZ);
LoggerHelper.Info($"[BenchMovement] 零点设定完成: X={HomeX}, Y={HomeY}, Z={HomeZ}");
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 零点设定失败: {ex.Message}");
ShowErrorMessageBox($"零点设定失败: {ex.Message}", () => { });
}
}
private async void OnReadStatus()
{
await ReadDeviceParametersAsync();
} }
#endregion #endregion
#region #region
private bool EnsureConnected()
{
if (_gantry == null || !_gantry.IsConnected)
{
ShowErrorMessageBox("设备未连接,请先连接台架", () => { });
return false;
}
return true;
}
private void UpdateConnectionStatus()
{
IsConnected = _gantry?.IsConnected ?? false;
ConnectionStatus = IsConnected ? "已连接" : "未连接";
}
private async Task ReadDeviceParametersAsync()
{
if (_gantry == null || !_gantry.IsConnected) return;
try
{
var (x, y, z) = await _gantry.GetHomePositionAsync();
HomeX = x; HomeY = y; HomeZ = z;
SpeedX = await _gantry.GetAxisSpeedAsync(1);
SpeedY = await _gantry.GetAxisSpeedAsync(2);
SpeedZ = await _gantry.GetAxisSpeedAsync(3);
HomeSpeed = await _gantry.GetHomePositionSpeedAsync();
// 读取 6 轴绝对位置
var positions = await _gantry.GetAllAbsolutePositionsAsync();
AbsPos1 = positions.Axis1; AbsPos2 = positions.Axis2; AbsPos3 = positions.Axis3;
AbsPos4 = positions.Axis4; AbsPos5 = positions.Axis5; AbsPos6 = positions.Axis6;
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 读取设备参数失败: {ex.Message}");
}
}
private async void OnPollTick(object? sender, EventArgs e)
{
if (_gantry == null || !_gantry.IsConnected) return;
try
{
// 批量读取 6 轴绝对位置(一次性通信,高效)
var pos = await _gantry.GetAllAbsolutePositionsAsync();
AbsPos1 = pos.Axis1; AbsPos2 = pos.Axis2; AbsPos3 = pos.Axis3;
AbsPos4 = pos.Axis4; AbsPos5 = pos.Axis5; AbsPos6 = pos.Axis6;
}
catch { /* 忽略轮询错误 */ }
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext) public override void OnNavigatedTo(NavigationContext navigationContext)
{ {
base.OnNavigatedTo(navigationContext); base.OnNavigatedTo(navigationContext);
if (!_isInitiated)
if (!_isInitiated && navigationContext.Parameters.ContainsKey("Name"))
{ {
_testStatus = navigationContext.Parameters.GetValue<string>("Name");
// TODO: 若后续需要从全局环境 Scope 字典中加载参数配置,可以在此处对齐获取:
// var scope = _globalInfo.ScopeDic[_testStatus];
_isInitiated = true; _isInitiated = true;
InitGantryFromDeviceManager();
}
}
/// <summary>
/// 从 DeviceManager.DeviceMap 中查找 GantryControlTcp 实例,
/// 优先使用当前打开的作用域,找不到则取任意已注册作用域。
/// </summary>
private void InitGantryFromDeviceManager()
{
try
{
// 优先使用当前打开的作用域
IScopedProvider? scope = null;
if (!string.IsNullOrEmpty(_globalInfo.CurrentOpeningScope)
&& _globalInfo.ScopeDic.TryGetValue(_globalInfo.CurrentOpeningScope, out scope))
{
// 找到了
}
else
{
// 退而求其次,取任意可用作用域
scope = _globalInfo.ScopeDic.Values.FirstOrDefault();
}
if (scope == null)
{
LoggerHelper.Warn("[BenchMovement] 无可用的作用域,无法解析 DeviceManager。");
return;
}
_deviceManager = scope.Resolve<DeviceManager>();
_gantry = _deviceManager.DeviceMap.Values.OfType<GantryControlTcp>().FirstOrDefault();
if (_gantry != null)
{
LoggerHelper.Info($"[BenchMovement] 已从 DeviceManager 获取 GantryControlTcp 实例。");
UpdateConnectionStatus();
}
else
{
LoggerHelper.Warn("[BenchMovement] DeviceManager 中未找到 GantryControlTcp 设备,请检查设备配置。");
}
}
catch (Exception ex)
{
LoggerHelper.Error($"[BenchMovement] 初始化 GantryControlTcp 失败: {ex.Message}");
} }
} }
public void Dispose() public void Dispose()
{ {
// 用于界面关闭或Region注销时的资源释放例如解除事件订阅、断开临时Socket通道、清空通信缓冲 _pollTimer.Stop();
// GantryControlTcp 由 DeviceManager 统一管理生命周期,此处仅停止轮询
} }
#endregion #endregion

View File

@@ -2,7 +2,6 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
@@ -44,7 +43,6 @@
<Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/>
</Style> </Style>
<!-- 极具机械/按钮交互感的现代工业 Button 样式 -->
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="Background" Value="#F5F5F5"/> <Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#333333"/> <Setter Property="Foreground" Value="#333333"/>
@@ -65,12 +63,10 @@
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border> </Border>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<!-- 鼠标悬停:变亮并加深边框 -->
<Trigger Property="IsMouseOver" Value="True"> <Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#EAEAEA"/> <Setter TargetName="btnBorder" Property="Background" Value="#EAEAEA"/>
<Setter TargetName="btnBorder" Property="BorderBrush" Value="#4A90E2"/> <Setter TargetName="btnBorder" Property="BorderBrush" Value="#4A90E2"/>
</Trigger> </Trigger>
<!-- 鼠标按下:明显的下凹和变暗交互感 -->
<Trigger Property="IsPressed" Value="True"> <Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#DCDCDC"/> <Setter TargetName="btnBorder" Property="Background" Value="#DCDCDC"/>
<Setter TargetName="btnBorder" Property="BorderBrush" Value="#2A6099"/> <Setter TargetName="btnBorder" Property="BorderBrush" Value="#2A6099"/>
@@ -81,7 +77,6 @@
</Setter> </Setter>
</Style> </Style>
<!-- 针对特殊控制按钮单独定制的样式 -->
<Style TargetType="Button" x:Key="StopButtonStyle" BasedOn="{StaticResource {x:Type Button}}"> <Style TargetType="Button" x:Key="StopButtonStyle" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#FFF0F0"/> <Setter Property="Background" Value="#FFF0F0"/>
<Setter Property="Foreground" Value="#C00000"/> <Setter Property="Foreground" Value="#C00000"/>
@@ -128,6 +123,29 @@
</Setter> </Setter>
</Style> </Style>
<Style TargetType="Button" x:Key="ConnectButtonStyle" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#5CB85C"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#4CAE4C"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#449D44"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#3D8B3D"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="GroupBox"> <Style TargetType="GroupBox">
<Setter Property="BorderBrush" Value="#D0D0D0"/> <Setter Property="BorderBrush" Value="#D0D0D0"/>
<Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderThickness" Value="1"/>
@@ -136,7 +154,6 @@
</Style> </Style>
</UserControl.Resources> </UserControl.Resources>
<!-- 浅灰、偏白色工业背景 -->
<Grid Background="#F0F2F5"> <Grid Background="#F0F2F5">
<TabControl Background="Transparent" BorderThickness="0"> <TabControl Background="Transparent" BorderThickness="0">
<TabControl.Resources> <TabControl.Resources>
@@ -150,90 +167,89 @@
</Style> </Style>
</TabControl.Resources> </TabControl.Resources>
<!-- ======================= 1. 主页标签页 ======================= --> <!-- ======================= 1. 控制面板 ======================= -->
<TabItem Header="标签"> <TabItem Header="控制">
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="2*"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="3*"/> <RowDefinition Height="*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/> <ColumnDefinition Width="4*"/>
<ColumnDefinition Width="1*"/> <ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 左上:台架位置 & 信号回读 --> <!-- 顶部:连接状态条 -->
<Grid Grid.Row="0" Grid.Column="0"> <Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Background="White" BorderBrush="#D0D0D0" BorderThickness="1" CornerRadius="3" Padding="15,8" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal">
<Ellipse Width="14" Height="14" Margin="0,0,8,0">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="#999999"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Fill" Value="#5CB85C"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="{Binding ConnectionStatus}" FontSize="16" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,20,0"/>
<Button Content="连接" Width="80" Margin="0" Style="{StaticResource ConnectButtonStyle}" Command="{Binding ConnectCommand}"/>
<Button Content="断开" Width="80" Margin="8,0,0,0" Style="{StaticResource StopButtonStyle}" Command="{Binding DisconnectCommand}"/>
<Button Content="刷新状态" Width="90" Margin="8,0,0,0" Command="{Binding ReadStatusCommand}"/>
</StackPanel>
</Border>
<!-- 左中:位置显示 & 寸动目标 -->
<Grid Grid.Row="1" Grid.Column="0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/> <ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1.5*"/> <ColumnDefinition Width="1.5*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 台架位置 --> <!-- 6轴绝对位置 -->
<GroupBox Grid.Column="0"> <GroupBox Grid.Column="0">
<GroupBox.Header> <GroupBox.Header>
<TextBlock Text="台架位置" Style="{StaticResource HeaderStyle}"/> <TextBlock Text="绝对位置(只读)" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header> </GroupBox.Header>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/> <RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X (mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Grid.Row="0" Grid.Column="0" Text="轴1" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding PosX}" Style="{StaticResource ValueStyle}"/> <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding AbsPos1}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y (mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Grid.Row="1" Grid.Column="0" Text="轴2" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding PosY}" Style="{StaticResource ValueStyle}"/> <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding AbsPos2}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z (mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Grid.Row="2" Grid.Column="0" Text="轴3" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding PosZ}" Style="{StaticResource ValueStyle}"/> <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding AbsPos3}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="轴4" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding AbsPos4}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="轴5" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding AbsPos5}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="5" Grid.Column="0" Text="轴6" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="5" Grid.Column="1" Text="{Binding AbsPos6}" Style="{StaticResource ValueStyle}"/>
</Grid> </Grid>
</GroupBox> </GroupBox>
<!-- 信号回读 --> <!-- 寸动目标位置 -->
<GroupBox Grid.Column="1"> <GroupBox Grid.Column="1">
<GroupBox.Header> <GroupBox.Header>
<TextBlock Text="信号回读" Style="{StaticResource HeaderStyle}"/> <TextBlock Text="寸动目标" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<UniformGrid Columns="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_X(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1X}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_Quality: " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1Quality}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_X(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2X}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_Quality: " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2Quality}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_Y(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1Y}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_Y(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2Y}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</UniformGrid>
</GroupBox>
</Grid>
<!-- 左中:扫描参数 -->
<GroupBox Grid.Row="1" Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="扫描参数" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header> </GroupBox.Header>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
@@ -245,286 +261,131 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X目标" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TargetX}" Margin="5"/>
<Button Grid.Row="0" Grid.Column="2" Content="移动" Width="60" Margin="5,0,5,0" Command="{Binding MoveXCommand}"/>
<Button Grid.Row="0" Grid.Column="3" Content="停止" Width="60" Margin="0,0,5,0" Style="{StaticResource StopButtonStyle}" Command="{Binding StopXCommand}"/>
<!-- X轴 --> <TextBlock Grid.Row="1" Grid.Column="0" Text="Y目标" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X移动步长(mm)" Style="{StaticResource LabelStyle}"/> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TargetY}" Margin="5"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ScanStepX}" Margin="5"/> <Button Grid.Row="1" Grid.Column="2" Content="移动" Width="60" Margin="5,0,5,0" Command="{Binding MoveYCommand}"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="X起始位置(mm)" Style="{StaticResource LabelStyle}"/> <Button Grid.Row="1" Grid.Column="3" Content="停止" Width="60" Margin="0,0,5,0" Style="{StaticResource StopButtonStyle}" Command="{Binding StopYCommand}"/>
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding ScanStartX}" Margin="5"/>
<TextBlock Grid.Row="0" Grid.Column="4" Text="X终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="5" Text="{Binding ScanEndX}" Margin="5"/>
<!-- Y轴 --> <TextBlock Grid.Row="2" Grid.Column="0" Text="Z目标" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y移动步长(mm)" Style="{StaticResource LabelStyle}"/> <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding TargetZ}" Margin="5"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ScanStepY}" Margin="5"/> <Button Grid.Row="2" Grid.Column="2" Content="移动" Width="60" Margin="5,0,5,0" Command="{Binding MoveZCommand}"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="Y起始位置(mm)" Style="{StaticResource LabelStyle}"/> <Button Grid.Row="2" Grid.Column="3" Content="停止" Width="60" Margin="0,0,5,0" Style="{StaticResource StopButtonStyle}" Command="{Binding StopZCommand}"/>
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding ScanStartY}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="4" Text="Y终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="5" Text="{Binding ScanEndY}" Margin="5"/>
<!-- Z轴 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z移动步长(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ScanStepZ}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="Z起始位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="3" Text="{Binding ScanStartZ}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="4" Text="Z终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="5" Text="{Binding ScanEndZ}" Margin="5"/>
</Grid>
</GroupBox>
<!-- 左下:寸动参数与雷达位置图 -->
<Grid Grid.Row="2" Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 寸动参数 -->
<GroupBox Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="寸动参数" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding JogX}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding JogY}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding JogZ}" Margin="5"/>
</Grid>
</GroupBox>
<!-- 坐标示意图 -->
<GroupBox Grid.Column="1">
<GroupBox.Header>
<TextBlock Text="台架现在位置" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid Margin="5" Background="White">
<!-- 十字坐标轴 -->
<Line X1="0" Y1="50" X2="200" Y2="50" Stroke="#B0B0B0" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="100"/>
<Line X1="100" Y1="0" X2="100" Y2="100" Stroke="#B0B0B0" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="100"/>
<!-- 轴向文本标签 -->
<TextBlock Text="X+" HorizontalAlignment="Center" VerticalAlignment="Top" Foreground="#666666" Margin="0,2,0,0"/>
<TextBlock Text="X-" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="#666666" Margin="0,0,0,2"/>
<TextBlock Text="Y+" HorizontalAlignment="Left" VerticalAlignment="Center" Foreground="#666666" Margin="2,0,0,0"/>
<TextBlock Text="Y-" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="#666666" Margin="0,0,2,0"/>
<!-- 台架模拟中心点 -->
<Border Width="65" Height="35" Background="#E9ECEF" BorderBrush="#999999" BorderThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="2">
<TextBlock Text="台架位置" Foreground="#333333" FontSize="11" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid> </Grid>
</GroupBox> </GroupBox>
</Grid> </Grid>
<!-- 右侧:控制面板除去Spacing增加Margin防粘连 --> <!-- 右侧:控制面板 -->
<Grid Grid.Row="0" Grid.RowSpan="3" Grid.Column="1" Margin="6,6,0,6"> <Grid Grid.Row="1" Grid.Column="1" Margin="6,6,0,6">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- 扫描路径操作 -->
<StackPanel Grid.Row="0"> <StackPanel Grid.Row="0">
<Button Content="▶ 开始扫描" Command="{Binding StartScanCommand}"/>
<Button Content="■ 停止扫描" Style="{StaticResource StopButtonStyle}" Command="{Binding StopScanCommand}"/>
<Button Content="↩ 回原点" Command="{Binding GoHomeCommand}"/> <Button Content="↩ 回原点" Command="{Binding GoHomeCommand}"/>
<TextBlock Text="扫描路径" Style="{StaticResource LabelStyle}" Margin="0,10,0,4"/> <Button Content="⬆ 三轴同动" Command="{Binding MoveAllCommand}"/>
<ComboBox Height="30" Background="White" Foreground="#333333" SelectedIndex="0" Margin="0,0,0,8" materialDesign:HintAssist.Hint="">
<ComboBoxItem Content="配置文件1.xml"/>
</ComboBox>
</StackPanel> </StackPanel>
<!-- 寸动单轴移动按钮组 --> <StackPanel Grid.Row="1" VerticalAlignment="Center">
<Grid Grid.Row="1"> <TextBlock Text="单轴控制" Style="{StaticResource LabelStyle}" HorizontalAlignment="Center" Margin="0,10,0,10"/>
<StackPanel VerticalAlignment="Center">
<Button Content="↕ X 移动" Command="{Binding MoveXCommand}"/> <Button Content="↕ X 移动" Command="{Binding MoveXCommand}"/>
<Button Content="↔ Y 移动" Command="{Binding MoveYCommand}"/> <Button Content="↔ Y 移动" Command="{Binding MoveYCommand}"/>
<Button Content="↕ Z 移动" Command="{Binding MoveZCommand}"/> <Button Content="↕ Z 移动" Command="{Binding MoveZCommand}"/>
</StackPanel> </StackPanel>
</Grid>
<!-- 急停 -->
<Button Grid.Row="2" Content="❌ 急 停" Style="{StaticResource EmergencyButtonStyle}" FontWeight="Bold" FontSize="18" Height="55" Margin="0" Command="{Binding EmergencyStopCommand}"/> <Button Grid.Row="2" Content="❌ 急 停" Style="{StaticResource EmergencyButtonStyle}" FontWeight="Bold" FontSize="18" Height="55" Margin="0" Command="{Binding EmergencyStopCommand}"/>
</Grid> </Grid>
</Grid> </Grid>
</TabItem> </TabItem>
<!-- ======================= 2. 设置标签页 ======================= --> <!-- ======================= 2. 设置 ======================= -->
<TabItem Header="设置"> <TabItem Header="设置">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10"> <StackPanel Margin="10">
<!-- 台架接口配置 --> <!-- 移动速度设定(脉冲) -->
<GroupBox> <GroupBox>
<GroupBox.Header>
<TextBlock Text="台架接口" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<StackPanel Orientation="Horizontal" Margin="5">
<StackPanel Orientation="Horizontal" Margin="0,0,15,0">
<TextBlock Text="IP: " Style="{StaticResource LabelStyle}"/>
<TextBox Width="150" Text="{Binding PlcIp}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,15,0">
<TextBlock Text="端口: " Style="{StaticResource LabelStyle}"/>
<TextBox Width="80" Text="{Binding PlcPort}"/>
</StackPanel>
<Button Content="应 用" Width="90" Margin="0" Command="{Binding ApplyNetworkCommand}"/>
</StackPanel>
</GroupBox>
<!-- 移动速度与零点设定 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 移动速度设定 -->
<GroupBox Grid.Column="0">
<GroupBox.Header> <GroupBox.Header>
<TextBlock Text="移动速度设定" Style="{StaticResource HeaderStyle}"/> <TextBlock Text="移动速度设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header> </GroupBox.Header>
<StackPanel Margin="5"> <StackPanel Margin="5">
<Grid Margin="0,0,0,6"> <Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/> <ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="X移动速度(mm/s)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="X速度(脉冲)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedX}"/> <TextBox Grid.Column="1" Text="{Binding SpeedX}"/>
</Grid> </Grid>
<Grid Margin="0,0,0,6"> <Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/> <ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="Y移动速度(mm/s)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="Y速度(脉冲)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedY}"/> <TextBox Grid.Column="1" Text="{Binding SpeedY}"/>
</Grid> </Grid>
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Z速度(脉冲)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedZ}"/>
</Grid>
<Grid Margin="0,0,0,10"> <Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/> <ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="Z移动速度(mm/s)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="回零速度(脉冲)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedZ}"/> <TextBox Grid.Column="1" Text="{Binding HomeSpeed}"/>
</Grid> </Grid>
<Button Content="速度设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetSpeedCommand}"/> <Button Content="速度设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetSpeedCommand}"/>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- 零点设定 --> <!-- 零点设定 -->
<GroupBox Grid.Column="1"> <GroupBox>
<GroupBox.Header> <GroupBox.Header>
<TextBlock Text="零点设定" Style="{StaticResource HeaderStyle}"/> <TextBlock Text="零点设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header> </GroupBox.Header>
<StackPanel Margin="5"> <StackPanel Margin="5">
<Grid Margin="0,0,0,6"> <Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="X零点(mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="X零点" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroX}"/> <TextBox Grid.Column="1" Text="{Binding HomeX}"/>
</Grid> </Grid>
<Grid Margin="0,0,0,6"> <Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="Y零点(mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="Y零点" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroY}"/> <TextBox Grid.Column="1" Text="{Binding HomeY}"/>
</Grid> </Grid>
<Grid Margin="0,0,0,10"> <Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/> <ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="Z零点(mm)" Style="{StaticResource LabelStyle}"/> <TextBlock Text="Z零点" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroZ}"/> <TextBox Grid.Column="1" Text="{Binding HomeZ}"/>
</Grid> </Grid>
<Button Content="零点设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetZeroCommand}"/> <Button Content="零点设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetHomeCommand}"/>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
</Grid>
<!-- CAN设定 -->
<GroupBox>
<GroupBox.Header>
<TextBlock Text="CAN设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="170"/>
<ColumnDefinition Width="280"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<!-- 通道与模式 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="应用程序CAN通道" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding CanChannel}" Margin="3"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="模式" Style="{StaticResource LabelStyle}" HorizontalAlignment="Center"/>
<ComboBox Grid.Row="0" Grid.Column="3" Height="28" Background="White" Foreground="#333333" SelectedIndex="0" Margin="3" materialDesign:HintAssist.Hint="">
<ComboBoxItem Content="正常模式"/>
</ComboBox>
<!-- 报文ID -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="报文ID(16进制)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding CanMsgId}" Margin="3"/>
<!-- 信号名称列表 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="报文名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding CanMsgName}" Margin="3"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="POS1_X信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1X}" Margin="3"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="POS2_X信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2X}" Margin="3"/>
<TextBlock Grid.Row="5" Grid.Column="0" Text="POS1_Y信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1Y}" Margin="3"/>
<TextBlock Grid.Row="6" Grid.Column="0" Text="POS2_Y信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2Y}" Margin="3"/>
<TextBlock Grid.Row="7" Grid.Column="0" Text="POS1_Quality信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1Quality}" Margin="3"/>
<TextBlock Grid.Row="8" Grid.Column="0" Text="POS2_Quality信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2Quality}" Margin="3"/>
</Grid>
</GroupBox>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>

View File

@@ -164,6 +164,7 @@ namespace CANModule.ViewModels
char lastChar = title[title.Length - 1]; char lastChar = title[title.Length - 1];
// 3. "TestCellX"转int // 3. "TestCellX"转int
MessageChannel = int.Parse(lastChar.ToString())-1; MessageChannel = int.Parse(lastChar.ToString())-1;
if (MessageChannel >= 4) MessageChannel -= 4;
RaisePropertyChanged(nameof(ConfigurationList)); RaisePropertyChanged(nameof(ConfigurationList));
} }
#endregion #endregion
@@ -204,6 +205,7 @@ namespace CANModule.ViewModels
{ {
CollectionID = Guid.NewGuid(), CollectionID = Guid.NewGuid(),
Channel = MessageChannel, Channel = MessageChannel,
MethodName = CANSignalBroadcaster.BuildMethodName((uint)decimalValue, SelectedSignal),
MessageName = ExtractMessageName(SelectedMessage), MessageName = ExtractMessageName(SelectedMessage),
SignalName = SelectedSignal, SignalName = SelectedSignal,
MessageID = decimalValue, MessageID = decimalValue,

View File

@@ -452,7 +452,7 @@ namespace CANModule.ViewModels
if (targetMsg != null) if (targetMsg != null)
{ {
_canfd.((uint)MessageSendChannel, targetMsg.msg_id.ToString(), SelectedSignal, SValue, sendPeriod); _canfd.((uint)MessageSendChannel, targetMsg.msg_id.ToString(), SelectedSignal, SValue, sendPeriod, IsDirectlySend);
} }
} }
} }
@@ -603,10 +603,10 @@ namespace CANModule.ViewModels
private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg) private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg)
{ {
// DBC 解码后的报文回调 // DBC 解码后的报文回调
Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.BeginInvoke(() =>
{ {
var msgName = System.Text.Encoding.Default.GetString(msg.strName).TrimEnd('\0'); var msgName = System.Text.Encoding.Default.GetString(msg.strName).TrimEnd('\0');
var find = CanMessageList.FirstOrDefault(s => s.ID == (int)msg.nID); var find = CanMessageList.FirstOrDefault(s => s.ID == (int)msg.nID&& s.==channel);
if (find != null) if (find != null)
{ {
find. = (byte)channel; find. = (byte)channel;

View File

@@ -9,7 +9,6 @@
<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>

View File

@@ -0,0 +1,20 @@
using CurveModule.Views;
using System.Reflection;
namespace CurveModule
{
public class CurveModule: IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<CurveRecallView>("CurveRecallView");
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,381 @@
using Logger;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Prism.Commands;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace CurveModule.ViewModels
{
/// <summary>
/// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
/// </summary>
public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
public PlotModel Plot { get; }
/// <summary>已加载的曲线通道列表</summary>
public ObservableCollection<CurveRecallChannelVM> Channels { get; } = new();
private CurveRecallChannelVM? _selectedChannel;
public CurveRecallChannelVM? SelectedChannel
{
get => _selectedChannel;
set => SetProperty(ref _selectedChannel, value);
}
private string _statusMessage = "请加载 CSV 文件以回读曲线";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
#endregion
#region
public ICommand LoadCsvCommand { get; }
public ICommand ResetViewCommand { get; }
public ICommand ClearCommand { get; }
#endregion
#region
// 颜色调色盘(与 MonitorView 一致)
private static readonly OxyColor[] _palette =
{
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
};
private int _colorIndex;
#endregion
public CurveRecallViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
Plot = BuildEmptyPlot();
LoadCsvCommand = new DelegateCommand(OnLoadCsv);
ResetViewCommand = new DelegateCommand(OnResetView);
ClearCommand = new DelegateCommand(OnClear);
}
#region
/// <summary>
/// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
/// </summary>
private void OnLoadCsv()
{
try
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
Title = "选择要加载的监测数据 CSV 文件"
};
if (dlg.ShowDialog() != true) return;
var records = ParseCsv(dlg.FileName);
if (records.Count == 0)
{
StatusMessage = "CSV 文件为空或格式不正确";
return;
}
// 按 (MonitorName, Scope) 分组
var groups = records.GroupBy(r => (r.MonitorName, r.Scope))
.OrderBy(g => g.Key.MonitorName)
.ThenBy(g => g.Key.Scope);
// 清空旧数据
ClearPlotAndChannels();
foreach (var group in groups)
{
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
string displayName = string.IsNullOrEmpty(group.Key.Scope)
? group.Key.MonitorName
: $"{group.Key.MonitorName} [{group.Key.Scope}]";
var channel = new CurveRecallChannelVM
{
MonitorName = group.Key.MonitorName,
Scope = group.Key.Scope,
DisplayName = displayName,
Color = color,
IsDisplayed = true
};
// 创建 LineSeries 并填充数据点
channel.Series = new LineSeries
{
Title = displayName,
Color = color,
StrokeThickness = 1.5
};
// 按时间排序
var sortedPoints = group.OrderBy(r => r.CreateTime);
foreach (var point in sortedPoints)
{
channel.Series.Points.Add(new DataPoint(point.CreateTime.ToOADate(), point.MonitorValue));
}
channel.PropertyChanged += OnChannelPropertyChanged;
Plot.Series.Add(channel.Series);
Channels.Add(channel);
}
Plot.InvalidatePlot(true);
StatusMessage = $"已加载 {Path.GetFileName(dlg.FileName)},共 {Channels.Count} 条曲线,{records.Count} 个数据点";
LoggerHelper.Info($"曲线回读:加载 {dlg.FileName}{Channels.Count} 条曲线,{records.Count} 个数据点");
}
catch (Exception ex)
{
LoggerHelper.Error($"加载 CSV 失败:{ex.Message}");
StatusMessage = $"加载失败:{ex.Message}";
}
}
private void OnResetView()
{
Plot.ResetAllAxes();
Plot.InvalidatePlot(false);
StatusMessage = "视图已复原";
}
private void OnClear()
{
ClearPlotAndChannels();
Plot.InvalidatePlot(true);
StatusMessage = "已清空,请重新加载 CSV 文件";
}
#endregion
#region CSV
/// <summary>
/// 解析 RecordView 导出的 CSV 文件,返回监测记录列表。
/// </summary>
private static List<MonitorRecord> ParseCsv(string path)
{
var results = new List<MonitorRecord>();
using var sr = new StreamReader(path, Encoding.UTF8);
// 读取表头
string? headerLine = sr.ReadLine();
if (headerLine == null) return results;
var headers = ParseCsvLine(headerLine);
int monitorNameIdx = headers.FindIndex(h => h.Trim() == "MonitorName");
int monitorValueIdx = headers.FindIndex(h => h.Trim() == "MonitorValue");
int scopeIdx = headers.FindIndex(h => h.Trim() == "Scope");
int createTimeIdx = headers.FindIndex(h => h.Trim() == "CreateTime");
if (monitorNameIdx < 0 || monitorValueIdx < 0 || scopeIdx < 0 || createTimeIdx < 0)
return results;
int maxIdx = Math.Max(monitorNameIdx, Math.Max(monitorValueIdx, Math.Max(scopeIdx, createTimeIdx)));
string? line;
while ((line = sr.ReadLine()) != null)
{
if (string.IsNullOrWhiteSpace(line)) continue;
var fields = ParseCsvLine(line);
if (fields.Count <= maxIdx) continue;
if (!double.TryParse(fields[monitorValueIdx].Trim(), out double value)) continue;
if (!DateTime.TryParse(fields[createTimeIdx].Trim(), out DateTime createTime)) continue;
results.Add(new MonitorRecord
{
MonitorName = fields[monitorNameIdx].Trim(),
MonitorValue = value,
Scope = fields[scopeIdx].Trim(),
CreateTime = createTime
});
}
return results;
}
/// <summary>
/// 解析单行 CSV正确处理引号转义。
/// </summary>
private static List<string> ParseCsvLine(string line)
{
var fields = new List<string>();
bool inQuotes = false;
var sb = new StringBuilder();
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (inQuotes)
{
if (c == '"')
{
if (i + 1 < line.Length && line[i + 1] == '"')
{
sb.Append('"');
i++;
}
else
{
inQuotes = false;
}
}
else
{
sb.Append(c);
}
}
else
{
if (c == '"')
{
inQuotes = true;
}
else if (c == ',')
{
fields.Add(sb.ToString());
sb.Clear();
}
else
{
sb.Append(c);
}
}
}
fields.Add(sb.ToString());
return fields;
}
#endregion
#region OxyPlot
/// <summary>
/// 构建空的 OxyPlot 模型(与 MonitorView 完全一致的配置)。
/// </summary>
private static PlotModel BuildEmptyPlot()
{
var pm = new PlotModel
{
Title = "曲线回读",
PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White
};
pm.Axes.Add(new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "时间",
StringFormat = "HH:mm:ss",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "值",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Legends.Add(new Legend
{
LegendPosition = LegendPosition.RightTop,
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
LegendBorder = OxyColors.LightGray
});
return pm;
}
#endregion
#region
private void OnChannelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(CurveRecallChannelVM.IsDisplayed)) return;
if (sender is not CurveRecallChannelVM channel) return;
if (channel.IsDisplayed)
{
if (channel.Series != null && !Plot.Series.Contains(channel.Series))
{
Plot.Series.Add(channel.Series);
}
}
else
{
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
}
}
Plot.InvalidatePlot(true);
}
private void ClearPlotAndChannels()
{
foreach (var ch in Channels)
{
ch.PropertyChanged -= OnChannelPropertyChanged;
}
Plot.Series.Clear();
Channels.Clear();
_colorIndex = 0;
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
}
public void Dispose()
{
ClearPlotAndChannels();
}
#endregion
}
/// <summary>
/// CSV 监测记录(单行数据)。
/// </summary>
internal class MonitorRecord
{
public string MonitorName { get; set; } = string.Empty;
public double MonitorValue { get; set; }
public string Scope { get; set; } = string.Empty;
public DateTime CreateTime { get; set; }
}
/// <summary>
/// 曲线回读通道MonitorName + Scope 唯一标识一条曲线。
/// </summary>
public class CurveRecallChannelVM : Prism.Mvvm.BindableBase
{
public string MonitorName { get; set; } = string.Empty;
public string Scope { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public OxyColor Color { get; set; } = OxyColors.SteelBlue;
private bool _isDisplayed = true;
public bool IsDisplayed
{
get => _isDisplayed;
set => SetProperty(ref _isDisplayed, value);
}
public LineSeries? Series { get; set; }
}
}

View File

@@ -0,0 +1,158 @@
<UserControl x:Class="CurveModule.Views.CurveRecallView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:oxy="http://oxyplot.org/wpf"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="700"
d:DesignWidth="1200">
<UserControl.Resources>
<converters:LessThanConverter x:Key="LessThanConverter"/>
</UserControl.Resources>
<Border Background="#F5F7FA">
<Grid x:Name="RootGrid" Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Row0标题 -->
<TextBlock Grid.Row="0"
Text="曲线回读"
FontSize="20" FontWeight="Bold"
Margin="4,0,0,8"/>
<!-- Row1工具栏 -->
<Border Grid.Row="1"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
CornerRadius="4" Padding="8" Margin="0,0,0,6">
<StackPanel Orientation="Horizontal">
<Button Content="📂 加载 CSV"
Command="{Binding LoadCsvCommand}"
Padding="12,4"/>
<Button Content="↺ 复原视图"
Command="{Binding ResetViewCommand}"
Padding="12,4" Margin="6,0,0,0"
ToolTip="按数据范围重置坐标轴缩放/平移"/>
<Button Content="✕ 清空"
Command="{Binding ClearCommand}"
Padding="12,4" Margin="6,0,0,0"
ToolTip="清空已加载的曲线数据"/>
</StackPanel>
</Border>
<!-- Row2主体 -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition>
<ColumnDefinition.Style>
<Style TargetType="ColumnDefinition">
<Setter Property="Width" Value="260"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
<Setter Property="Width" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ColumnDefinition.Style>
</ColumnDefinition>
<ColumnDefinition>
<ColumnDefinition.Style>
<Style TargetType="ColumnDefinition">
<Setter Property="Width" Value="6"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ActualWidth, ElementName=RootGrid, Converter={StaticResource LessThanConverter}, ConverterParameter=600}" Value="True">
<Setter Property="Width" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ColumnDefinition.Style>
</ColumnDefinition>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧:通道列表 -->
<Border Grid.Column="0"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#ECEFF4" Padding="8,4">
<TextBlock Text="曲线通道(勾选显示/隐藏)" FontWeight="Bold"/>
</Border>
<ListBox Grid.Row="1"
ItemsSource="{Binding Channels}"
SelectedItem="{Binding SelectedChannel}"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="2">
<DockPanel>
<CheckBox IsChecked="{Binding IsDisplayed}"
VerticalAlignment="Center"
ToolTip="勾选=在图表上显示,取消=隐藏"/>
<Border Width="10" Height="10"
CornerRadius="2"
VerticalAlignment="Center"
Margin="4,0,6,0">
<Border.Background>
<SolidColorBrush Color="SteelBlue"/>
</Border.Background>
</Border>
<TextBlock Text="{Binding DisplayName}"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</DockPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="2"
Text="{Binding StatusMessage}"
FontSize="11" Foreground="#666"
Margin="4,2" TextWrapping="Wrap"/>
</Grid>
</Border>
<GridSplitter Grid.Column="1"
HorizontalAlignment="Stretch"
Background="Transparent"/>
<!-- 右侧OxyPlot 图表 -->
<Border Grid.Column="2"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
CornerRadius="4">
<oxy:PlotView Model="{Binding Plot}"
Background="Transparent"/>
</Border>
</Grid>
<!-- Row3状态栏 -->
<Border Grid.Row="3"
Background="#ECEFF4"
Padding="8,4" Margin="0,6,0,0"
CornerRadius="2">
<TextBlock Text="{Binding StatusMessage}"
Foreground="#444"
FontSize="12"/>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace CurveModule.Views
{
/// <summary>
/// CurveRecallView.xaml 的交互逻辑
/// </summary>
public partial class CurveRecallView : UserControl
{
public CurveRecallView()
{
InitializeComponent();
}
}
}

View File

@@ -1,4 +1,5 @@
using NModbus; using Model.Models;
using NModbus;
using System; using System;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
@@ -19,7 +20,11 @@ namespace DeviceCommand.Base
public bool IsConnected => _tcpClient?.Connected ?? false; public bool IsConnected => _tcpClient?.Connected ?? false;
protected readonly SemaphoreSlim _commLock = new(1, 1); protected readonly SemaphoreSlim _commLock = new(1, 1);
public ModbusTcp(TcpConfig config) : this()
{
if (config == null) return;
ConfigureDevice(config.IPAddress, config.Port, config.SendTimeout, config.ReceiveTimeout);
}
public ModbusTcp() public ModbusTcp()
{ {
_tcpClient = new TcpClient(); _tcpClient = new TcpClient();

View File

@@ -230,7 +230,89 @@ namespace DeviceCommand.Base
_commLock.Release(); _commLock.Release();
} }
} }
#region ( SCPI )
/// <summary>
/// 无锁核心方法:发送命令并一次性读取所有回传的二进制原始数据包(不进行 # 协议头解析,专用于读取纯文件流如 PNG
/// </summary>
private async Task<byte[]> LoglessReadAllBytesAsync(string queryCommand, CancellationToken ct)
{
if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
// 1. 发送查询命令(例如 :PRINt? PNG\n
await LoglessSendAsync(Encoding.UTF8.GetBytes(queryCommand), ct);
NetworkStream stream = _tcpClient.GetStream();
// 2. 鼎阳示波器的截图数据大概在 100KB - 800KB 左右,使用动态内存流接收整个包
using (var ms = new MemoryStream())
{
byte[] buffer = new byte[8192]; // 8KB 缓冲区
try
{
// 先给设备短暂的反应时间,等待数据到达网络缓冲区
int delayCount = 0;
while (!_tcpClient.GetStream().DataAvailable && delayCount < 50)
{
await Task.Delay(10, ct);
delayCount++;
}
// 循环读取,直到网络流中没有更多数据
do
{
ct.ThrowIfCancellationRequested();
int read = ReceiveTimeout > 0
? await stream.ReadAsync(buffer, 0, buffer.Length, ct)
.WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
.ConfigureAwait(false)
: await stream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
if (read == 0) break; // 远程流关闭
ms.Write(buffer, 0, read);
// 如果流里没有剩余数据了,退出读取(防止 ReadAsync 在没有数据时无限阻塞等待)
if (!stream.DataAvailable)
{
// 极短延时再确认一次,防止分包网络延迟引起的“假结束”
await Task.Delay(30, ct);
if (!stream.DataAvailable)
{
break;
}
}
} while (true);
return ms.ToArray();
}
catch (TimeoutException ex)
{
await ResetConnectionAsync(ct);
throw new TimeoutException($"读取二进制大包超时(等待:{ReceiveTimeout} ms链路已重置。", ex);
}
}
}
/// <summary>
/// 【公开方法】发送命令并读取设备回传的全部原始二进制字节数组(不带协议头解析,直接返回整个字节缓冲区)
/// </summary>
public async Task<byte[]> ReadAllBytesAsync(string queryCommand, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
return await LoglessReadAllBytesAsync(queryCommand, ct);
}
finally
{
_commLock.Release();
}
}
#endregion
public void Dispose() public void Dispose()
{ {
_tcpClient?.Dispose(); _tcpClient?.Dispose();

View File

@@ -0,0 +1,540 @@
using Common.Attributes;
using DeviceCommand.Base;
using Model.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace DeviceCommand.Devices
{
[ADPCommand]
public class GantryControlTcp : ModbusTcp
{
private readonly byte _slaveAddress=1;
// 向后兼容TcpDevice 即自身(继承自 ModbusTcp
public ModbusTcp TcpDevice => this;
public GantryControlTcp(TcpConfig config) : base(config)
{
}
// =================================================================
// ------------------ Coil / Bit 状态与控制 (位元件) ------------------
// =================================================================
private const ushort ADDR_M_ESTOP = 20496; // 0x5010: 紧停
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_Z_STOP = 43; // 0x2B: Z轴停止
private const ushort ADDR_M_X_START = 30; // 0x1E: X轴指定位置启动
private const ushort ADDR_M_Y_START = 31; // 0x1F: Y轴指定位置启动
private const ushort ADDR_M_Z_START = 35; // 0x23: Z轴指定位置启动
private const ushort ADDR_M_HOME_TRIGGER = 100; // 0x64: 回原点触发
// =================================================================
// ------------------ 轴使能控制 (HM 寄存器) ------------------
// =================================================================
private const ushort ADDR_HM_AXIS1_ENABLE = 49408; // 0xC100: 轴1使能 (X轴)
private const ushort ADDR_HM_AXIS2_ENABLE = 49409; // 0xC101: 轴2使能 (Y轴)
private const ushort ADDR_HM_AXIS3_ENABLE = 49410; // 0xC102: 轴3使能 (Z轴)
private const ushort ADDR_HM_AXIS4_ENABLE = 49411; // 0xC103: 轴4使能
private const ushort ADDR_HM_AXIS5_ENABLE = 49412; // 0xC104: 轴5使能
private const ushort ADDR_HM_AXIS6_ENABLE = 49413; // 0xC105: 轴6使能
// =================================================================
// ------------------ 速度与位置参数 (HD 32位数据寄存器) --------------
// =================================================================
private const ushort ADDR_HD_HOME_POS_SPEED = 41288; // 0xA148: 原点定位速度 (32位)
// 轴原点位置
private const ushort ADDR_HD_X_HOME_POS = 41198; // 0xA0EE: X轴原点位 (32位)
private const ushort ADDR_HD_Z_HOME_POS = 41208; // 0xA0F8: Z轴原点位 (32位)
private const ushort ADDR_HD_Y_HOME_POS = 41218; // 0xA102: Y轴原点位 (32位)
// 轴运行目标位置
private const ushort ADDR_HD_Y_TARGET = 41228; // 0xA10C: Y轴指定位置 (32位)
private const ushort ADDR_HD_Z_TARGET = 41238; // 0xA116: Z轴指定位置 (32位)
private const ushort ADDR_HD_X_TARGET = 41248; // 0xA120: X轴指定位置 (32位)
// 轴运行速度
private const ushort ADDR_HD_X_SPEED = 41318; // 0xA166: X轴速度 (32位)
private const ushort ADDR_HD_Z_SPEED = 41328; // 0xA170: Z轴速度 (32位)
private const ushort ADDR_HD_Y_SPEED = 41338; // 0xA17A: Y轴速度 (32位)
// =================================================================
// ------------------ 轴绝对位置(只读,相对原点)------------------
// =================================================================
private const ushort ADDR_ABS_AXIS1_POS = 47232; // 轴1绝对位置 (32位)
private const ushort ADDR_ABS_AXIS2_POS = 47236; // 轴2绝对位置 (32位)
private const ushort ADDR_ABS_AXIS3_POS = 47240; // 轴3绝对位置 (32位)
private const ushort ADDR_ABS_AXIS4_POS = 47244; // 轴4绝对位置 (32位)
private const ushort ADDR_ABS_AXIS5_POS = 47248; // 轴5绝对位置 (32位)
private const ushort ADDR_ABS_AXIS6_POS = 47252; // 轴6绝对位置 (32位)
// =================================================================
// ------------------ 核心控制逻辑与参数读写 ------------------------
// =================================================================
/// <summary>
/// 轴使能控制
/// </summary>
/// <param name="axis">轴编号 (1 - 6)</param>
public async Task SetAxisEnableAsync(int axis, bool enable, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_HM_AXIS1_ENABLE,
2 => ADDR_HM_AXIS2_ENABLE,
3 => ADDR_HM_AXIS3_ENABLE,
4 => ADDR_HM_AXIS4_ENABLE,
5 => ADDR_HM_AXIS5_ENABLE,
6 => ADDR_HM_AXIS6_ENABLE,
_ => throw new ArgumentException("轴通道错误,仅支持 1 - 6", nameof(axis))
};
await WriteSingleCoilAsync(_slaveAddress, address, enable, ct);
}
/// <summary>
/// 读取指定轴的使能状态
/// </summary>
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
public async Task<bool> GetAxisEnableAsync(int axis, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_HM_AXIS1_ENABLE,
2 => ADDR_HM_AXIS2_ENABLE,
3 => ADDR_HM_AXIS3_ENABLE,
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
};
bool[] coils = await ReadCoilsAsync(_slaveAddress, address, 1, ct);
return coils != null && coils.Length > 0 && coils[0];
}
/// <summary>
/// 同时设置 X/Y/Z 轴的原点位置(带各自独立的范围检查)
/// </summary>
/// <param name="xPos">X轴原点位置 (范围: 0 ~ 50000)</param>
/// <param name="yPos">Y轴原点位置 (范围: 0 ~ 50000)</param>
/// <param name="zPos">Z轴原点位置 (范围: 0 ~ 20000)</param>
public async Task SetHomePositionAsync(int xPos, int yPos, int zPos, CancellationToken ct = default)
{
if (xPos < 0 || xPos > 50000)
throw new ArgumentOutOfRangeException(nameof(xPos), $"X轴原点设置值 [{xPos}] 超出合法范围 (0 - 50000)");
if (yPos < 0 || yPos > 50000)
throw new ArgumentOutOfRangeException(nameof(yPos), $"Y轴原点设置值 [{yPos}] 超出合法范围 (0 - 50000)");
if (zPos < 0 || zPos > 20000)
throw new ArgumentOutOfRangeException(nameof(zPos), $"Z轴原点设置值 [{zPos}] 超出合法范围 (0 - 20000)");
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, Int32ToUshorts(xPos), ct);
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_HOME_POS, Int32ToUshorts(yPos), ct);
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_HOME_POS, Int32ToUshorts(zPos), ct);
}
/// <summary>
/// 批量查询当前 X、Y、Z 三轴设定的原点位置值
/// </summary>
/// <returns>返回包含三轴原点位置的元组 (X, Y, Z)</returns>
public async Task<(int X, int Y, int Z)> GetHomePositionAsync(CancellationToken ct = default)
{
// 41218(Y) - 41198(X) = 20加Y轴自身的 2 个寄存器,共批量读取 22 个连续寄存器
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, 22, ct);
if (registers == null || registers.Length < 22)
throw new InvalidOperationException("从 PLC 读取三轴原点位置失败,返回数据长度不足。");
int xHomePos = UshortsToInt32(new ushort[] { registers[0], registers[1] });
int zHomePos = UshortsToInt32(new ushort[] { registers[10], registers[11] });
int yHomePos = UshortsToInt32(new ushort[] { registers[20], registers[21] });
return (xHomePos, yHomePos, zHomePos);
}
/// <summary>
/// 设置回原点定位速度(带 0 - 100000 范围检查)
/// </summary>
public async Task SetHomePositionSpeedAsync(int speed, CancellationToken ct = default)
{
if (speed < 0 || speed > 100000)
throw new ArgumentOutOfRangeException(nameof(speed), $"回原点定位速度值 [{speed}] 超出合法范围 (0 - 100000)");
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, Int32ToUshorts(speed), ct);
}
/// <summary>
/// 查询当前设定的回原点定位速度
/// </summary>
public async Task<int> GetHomePositionSpeedAsync(CancellationToken ct = default)
{
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, 2, ct);
if (registers == null || registers.Length < 2)
throw new InvalidOperationException("从 PLC 读取回原点定位速度失败,返回数据长度不足。");
return UshortsToInt32(registers);
}
/// <summary>
/// 设置指定轴的运行速度(带 0 - 100000 范围检查)
/// </summary>
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
public async Task SetAxisSpeedAsync(int axis, int speed, CancellationToken ct = default)
{
if (speed < 0 || speed > 100000)
throw new ArgumentOutOfRangeException(nameof(speed), $"轴速度设置值 [{speed}] 超出合法范围 (0 - 100000)");
ushort address = axis switch
{
1 => ADDR_HD_X_SPEED,
2 => ADDR_HD_Y_SPEED,
3 => ADDR_HD_Z_SPEED,
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
};
await WriteMultipleRegistersAsync(_slaveAddress, address, Int32ToUshorts(speed), ct);
}
/// <summary>
/// 读取指定轴当前设定的运行速度
/// </summary>
public async Task<int> GetAxisSpeedAsync(int axis, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_HD_X_SPEED,
2 => ADDR_HD_Y_SPEED,
3 => ADDR_HD_Z_SPEED,
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
};
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
if (registers == null || registers.Length < 2)
throw new InvalidOperationException($"从 PLC 读取轴 {axis} 速度失败。");
return UshortsToInt32(registers);
}
/// <summary>
/// 写入指定轴的目标位置(动态安全校验:目标位置不能超过 [轴最大物理极限 - 当前轴原点设置值]
/// </summary>
public async Task SetAxisTargetPositionAsync(int axis, int targetPos, CancellationToken ct = default)
{
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
ushort targetAddress;
int maxLimit;
int currentHomePos;
switch (axis)
{
case 1:
targetAddress = ADDR_HD_X_TARGET;
maxLimit = 50000;
currentHomePos = xHome;
break;
case 2:
targetAddress = ADDR_HD_Y_TARGET;
maxLimit = 50000;
currentHomePos = yHome;
break;
case 3:
targetAddress = ADDR_HD_Z_TARGET;
maxLimit = 20000;
currentHomePos = zHome;
break;
default:
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis));
}
int allowedMaxLimit = maxLimit - currentHomePos;
if (targetPos < 0 || targetPos > allowedMaxLimit)
{
throw new ArgumentOutOfRangeException(
nameof(targetPos),
$"轴 {axis} 目标位置 [{targetPos}] 不合法!当前原点为 [{currentHomePos}],允许的有效写入范围为: 0 - {allowedMaxLimit}"
);
}
await WriteMultipleRegistersAsync(_slaveAddress, targetAddress, Int32ToUshorts(targetPos), ct);
}
/// <summary>
/// 读取指定轴当前设定的目标指定位置
/// </summary>
public async Task<int> GetAxisTargetPositionAsync(int axis, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_HD_X_TARGET,
2 => ADDR_HD_Y_TARGET,
3 => ADDR_HD_Z_TARGET,
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
};
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
if (registers == null || registers.Length < 2)
throw new InvalidOperationException($"从 PLC 读取轴 {axis} 目标位置失败。");
return UshortsToInt32(registers);
}
/// <summary>
/// 设置系统紧停锁定状态 (true: 锁定激活, false: 解除锁定)
/// </summary>
public async Task SetEStopStateAsync(bool state, CancellationToken ct = default)
{
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_ESTOP, state, ct);
}
/// <summary>
/// 设置指定轴的停止控制状态 (true: 持续输出停止信号, false: 释放停止信号)
/// </summary>
public async Task SetStopStateAsync(int axis, bool state, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_M_X_STOP,
2 => ADDR_M_Y_STOP,
3 => ADDR_M_Z_STOP,
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
};
await WriteSingleCoilAsync(_slaveAddress, address, state, ct);
}
/// <summary>
/// 全局回零(安全校验版:先检查回原点速度是否为 0为0则拒绝触发
/// </summary>
public async Task HomeAsync(CancellationToken ct = default)
{
int currentSpeed = await GetHomePositionSpeedAsync(ct);
if (currentSpeed == 0)
throw new InvalidOperationException("无法触发回原点!当前回原点定位速度为 0请先设置合法的回零速度。");
await TriggerCoilAsync(ADDR_M_HOME_TRIGGER, ct);
}
/// <summary>
/// 单轴指定位置启动(含速度判定、使能自动补齐、动作目标位置独立范围校验)
/// </summary>
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
public async Task StartSingleAxisAsync(int axis, CancellationToken ct = default)
{
ushort startAddress;
int maxLimit;
int currentHomePos;
// 1. 获取基础参数与校验边界
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
switch (axis)
{
case 1:
startAddress = ADDR_M_X_START;
maxLimit = 50000;
currentHomePos = xHome;
break;
case 2:
startAddress = ADDR_M_Y_START;
maxLimit = 50000;
currentHomePos = yHome;
break;
case 3:
startAddress = ADDR_M_Z_START;
maxLimit = 20000;
currentHomePos = zHome;
break;
default:
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis));
}
// 2. 检查动作轴的速度
int currentSpeed = await GetAxisSpeedAsync(axis, ct);
if (currentSpeed == 0)
throw new InvalidOperationException($"轴 {axis} 无法启动!当前设定速度为 0。");
// 3. 检查动作轴使能,若为 false 则自动补写 true 激活驱动器
bool isEnabled = await GetAxisEnableAsync(axis, ct);
if (!isEnabled)
{
await SetAxisEnableAsync(axis, true, ct);
await Task.Delay(100, ct); // 等待硬件继电器响应与伺服驱动就绪
}
// 4. 精准独立校验当前需要动作轴的目标定位是否越界
int currentTarget = await GetAxisTargetPositionAsync(axis, ct);
int allowedMaxLimit = maxLimit - currentHomePos;
if (currentTarget < 0 || currentTarget > allowedMaxLimit)
{
throw new ArgumentOutOfRangeException($"轴 {axis} 无法启动!当前指定目标位置 [{currentTarget}] 超过了允许的最大安全极限值 [{allowedMaxLimit}]。");
}
// 5. 校验完成,边缘触发点动启动
await TriggerCoilAsync(startAddress, ct);
}
/// <summary>
/// 三轴同时启动全开严格校验全部运行轴速度、批量检测与补正1~6号轴使能、联动校验三轴目标范围极限
/// </summary>
public async Task StartAllAxesAsync(CancellationToken ct = default)
{
// 1. 一次性读取三轴速度,严禁任何一轴速度为 0
int xSpeed = await GetAxisSpeedAsync(1, ct);
int ySpeed = await GetAxisSpeedAsync(2, ct);
int zSpeed = await GetAxisSpeedAsync(3, ct);
if (xSpeed == 0 || ySpeed == 0 || zSpeed == 0)
throw new InvalidOperationException($"无法全开启动!存在运行速度为 0 的轴。当前速度 -> X:{xSpeed}, Y:{ySpeed}, Z:{zSpeed}");
// 2. 批量读取 1 ~ 6 号轴连续的使能状态线圈,极大地优化网络性能
bool[] enableStates = await ReadCoilsAsync(_slaveAddress, ADDR_HM_AXIS1_ENABLE, 6, ct);
if (enableStates == null || enableStates.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++)
{
if (!enableStates[i])
{
await WriteSingleCoilAsync(_slaveAddress, enableAddresses[i], true, ct);
hasModifiedEnable = true;
}
}
// 如果执行过使能补齐补齐写入在循环外进行统一延时保证PLC时序安全
if (hasModifiedEnable)
await Task.Delay(100, ct);
// 3. 获取三轴原点及当前设定的目标指定位置
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
int xTarget = await GetAxisTargetPositionAsync(1, ct);
int yTarget = await GetAxisTargetPositionAsync(2, ct);
int zTarget = await GetAxisTargetPositionAsync(3, ct);
// X轴安全范围联动校验
int xMaxAllowed = 50000 - xHome;
if (xTarget < 0 || xTarget > xMaxAllowed)
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);
}
#region
/// <summary>
/// 触发点动脉冲信号(置 1 后,等待 100ms 自动置 0 释放)
/// </summary>
private async Task TriggerCoilAsync(ushort address, CancellationToken ct = default)
{
await WriteSingleCoilAsync(_slaveAddress, address, true, ct);
await Task.Delay(100, ct);
await WriteSingleCoilAsync(_slaveAddress, address, false, ct);
}
/// <summary>
/// 将 32 位整型数据转换为 Modbus 的 2 个 16 位无符号整数(低字在前 CDAB 格式)
/// </summary>
private ushort[] Int32ToUshorts(int value)
{
ushort lowWord = (ushort)(value & 0xFFFF);
ushort highWord = (ushort)((value >> 16) & 0xFFFF);
return new ushort[] { lowWord, highWord };
}
/// <summary>
/// 将 Modbus 的 2 个 16 位无符号整数转换为 32 位整型(低字在前 CDAB 格式)
/// </summary>
private int UshortsToInt32(ushort[] registers)
{
uint lowWord = registers[0];
uint highWord = registers[1];
return (int)((highWord << 16) | lowWord);
}
#endregion
#region
/// <summary>
/// 读取指定轴的绝对位置(相对原点的实时位置,只读)
/// </summary>
/// <param name="axis">轴编号 (1-6)</param>
public async Task<int> GetAbsolutePositionAsync(int axis, CancellationToken ct = default)
{
ushort address = axis switch
{
1 => ADDR_ABS_AXIS1_POS,
2 => ADDR_ABS_AXIS2_POS,
3 => ADDR_ABS_AXIS3_POS,
4 => ADDR_ABS_AXIS4_POS,
5 => ADDR_ABS_AXIS5_POS,
6 => ADDR_ABS_AXIS6_POS,
_ => throw new ArgumentException("轴编号错误,仅支持 1-6", nameof(axis))
};
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
if (registers == null || registers.Length < 2)
throw new InvalidOperationException($"读取轴 {axis} 绝对位置失败。");
return UshortsToInt32(registers);
}
/// <summary>
/// 批量读取 1-6 轴的绝对位置(一次性读取 22 个寄存器,高效)
/// </summary>
/// <returns>返回 6 轴绝对位置元组</returns>
public async Task<(int Axis1, int Axis2, int Axis3, int Axis4, int Axis5, int Axis6)> GetAllAbsolutePositionsAsync(CancellationToken ct = default)
{
// 从 47232 开始连续读取 22 个寄存器(覆盖 47232-47253
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_ABS_AXIS1_POS, 22, ct);
if (registers == null || registers.Length < 22)
throw new InvalidOperationException("批量读取 6 轴绝对位置失败,返回数据长度不足。");
// 每轴间隔 4 个寄存器2个数据 + 2个间隙提取各轴数据
int axis1 = UshortsToInt32(new ushort[] { registers[0], registers[1] });
int axis2 = UshortsToInt32(new ushort[] { registers[4], registers[5] });
int axis3 = UshortsToInt32(new ushort[] { registers[8], registers[9] });
int axis4 = UshortsToInt32(new ushort[] { registers[12], registers[13] });
int axis5 = UshortsToInt32(new ushort[] { registers[16], registers[17] });
int axis6 = UshortsToInt32(new ushort[] { registers[20], registers[21] });
return (axis1, axis2, axis3, axis4, axis5, axis6);
}
#endregion
}
}

View File

@@ -1,5 +1,6 @@
using Common.Attributes; using Common.Attributes;
using DeviceCommand.Base; using DeviceCommand.Base;
using Model.Models;
using NModbus; using NModbus;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -8,15 +9,15 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] //[ADPCommand]
public class IOBoard : ModbusTcp public class IOBoard : ModbusTcp
{ {
//只有两个,八台产品公用两两分组各用一个
public IOBoard(string Ip地址, int , int , int ) public IOBoard(TcpConfig config) : base(config)
{ {
ConfigureDevice(Ip地址, , , );
} }

View File

@@ -0,0 +1,160 @@
using Common.Attributes;
using Model.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
using DeviceCommand.Base;
using DeviceCommand.Devices;
using System.ComponentModel;
namespace DeviceCommand.Devices
{
/// <summary>
/// 系统控制功能类型枚举 (中文)
/// </summary>
public enum : byte
{
,
,
,
}
/// <summary>
/// 精简后的 IO 端口信息结构体
/// </summary>
public struct IoPortInfo
{
public int ModuleIndex { get; set; } // IO板卡/模块序号 (1 或 2)
public ushort Address { get; set; } // 转换为 Modbus 的线圈相对偏移地址 (Y1->0, Y2->1)
public IoPortInfo(int moduleIndex, ushort address)
{
ModuleIndex = moduleIndex;
Address = address;
}
}
[ADPCommand]
public class IOBoardGroup
{
// 声明底层的 2 个 IO 板卡实例
public IOBoard Board1 { get; private set; }
public IOBoard Board2 { get; private set; }
private readonly byte _slaveAddress = 1; // 默认 Modbus 从站号为 1
// 核心:严格对齐图片数据的双层嵌套配置字典 (2个模块1-8台架)
private static readonly Dictionary<int, Dictionary<, IoPortInfo>> IoMapping
= new Dictionary<int, Dictionary<, IoPortInfo>>()
{
{ 1, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(1, 5) }, // Y6 -> 5
{ ., new IoPortInfo(1, 0) }, // Y1 -> 0
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 15) } // Y9 -> 8
}},
{ 2, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(1, 6) }, // Y7 -> 6
{ ., new IoPortInfo(1, 2) }, // Y3 -> 2
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 14) } // Y10 -> 9
}},
{ 3, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(1, 7) }, // Y8 -> 7
{ ., new IoPortInfo(1, 3) }, // Y4 -> 3
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 13) } // Y11 -> 10
}},
{ 4, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(1, 8) }, // Y9 -> 8
{ ., new IoPortInfo(1, 4) }, // Y5 -> 4
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 12) } // Y12 -> 11
}},
{ 5, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(2, 4) }, // Y5 -> 4
{ ., new IoPortInfo(2, 0) }, // Y1 -> 0
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 11) } // Y13 -> 12
}},
{ 6, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(2, 5) }, // Y6 -> 5
{ ., new IoPortInfo(2, 1) }, // Y2 -> 1
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 10) } // Y14 -> 13
}},
{ 7, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(2, 6) }, // Y7 -> 6
{ ., new IoPortInfo(2, 2) }, // Y3 -> 2
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 9) } // Y15 -> 14
}},
{ 8, new Dictionary<, IoPortInfo> {
{ ., new IoPortInfo(2, 7) }, // Y8 -> 7
{ ., new IoPortInfo(2, 3) }, // Y4 -> 3
{ ., new IoPortInfo(1, 1) }, // Y2 -> 1
{ ., new IoPortInfo(2, 8) } // Y16 -> 15
}}
};
public IOBoardGroup(IOBoard board1, IOBoard board2)
{
Board1 = board1 ?? throw new ArgumentNullException(nameof(board1), "IOBoard Board1 实例不能为空。");
Board2 = board2 ?? throw new ArgumentNullException(nameof(board2), "IOBoard Board2 实例不能为空。");
}
/// <summary>
/// 执行指定台架的功能动作控制(带高低电平开关及底层安全互锁)
/// </summary>
/// <param name="台架序号">台架/工位序号 (1 - 8)</param>
/// <param name="动作">控制功能类型 (单相充电, 抛负载, 短路测试, 蜂鸣器报警)</param>
/// <param name="开关状态">开关状态 (true: 开启吸合, false: 关闭断开)</param>
/// <param name="取消令牌">异步取消令牌</param>
public async Task Async(int , , bool , CancellationToken = default)
{
// 1. 根据传入的台架序号和动作检索字典映射关系
if (!IoMapping.TryGetValue(, out var ) || !.TryGetValue(, out var io信息))
{
throw new ArgumentException($"未找到台架 [{台架序号}] 对应功能 [{动作}] 的 IO 点位映射配置。");
}
// 2. 软件防错高危互锁逻辑
if ()
{
if ( == .)
{
var = IoMapping[][.];
await GetBoardInstance(.ModuleIndex).(_slaveAddress, .Address, false);
}
else if ( == .)
{
var = IoMapping[][.];
await GetBoardInstance(.ModuleIndex).(_slaveAddress, .Address, false);
}
}
// 3. 动态获取目标板卡实例并直接驱动开关端口
IOBoard = GetBoardInstance(io信息.ModuleIndex);
await .(_slaveAddress, io信息.Address, );
}
/// <summary>
/// 内部路由辅助方法:根据板卡索引安全获取当前连接的实例对象
/// </summary>
private IOBoard GetBoardInstance(int moduleIndex)
{
IOBoard board = moduleIndex switch
{
1 => Board1,
2 => Board2,
_ => throw new ArgumentOutOfRangeException(nameof(moduleIndex), "非法的板卡序号,系统仅支持 1# 和 2# 模块。")
};
return board ?? throw new InvalidOperationException($"IO板卡模块 [{moduleIndex}#] 尚未完成初始化,无法建立总线通信。");
}
}
}

View File

@@ -6,54 +6,70 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
#region
/// <summary>
/// 电源电气/耦合模式
/// </summary>
public enum PowerCouplingMode
{
/// <summary> 纯交流模式 </summary>
AC,
/// <summary> 纯直流模式 </summary>
DC,
/// <summary> 交直流混合模式 </summary>
ACDC,
/// <summary> 直交流混合模式 </summary>
DCAC
}
/// <summary>
/// 设备操作运行角色/模式
/// </summary>
public enum DeviceOperationMode
{
/// <summary> 电压源模式(常规源) </summary>
VOLTage,
/// <summary> 电子负载模式(放电拉载) </summary>
LOAD,
/// <summary> 电流源模式 </summary>
CURRent
}
#endregion
[ADPCommand] [ADPCommand]
public class IT7800E : Tcp public class IT7800E : Tcp
{ {
//只有一个,八台产品一起用一个 // 八台产品用一个,使用换行符 \n (ASCII 0x0A) 作为 SCPI 结束符[cite: 1]
// 根据通用 SCPI 指令规范,使用换行符 (ASCII 字符 LF即 \n) 作为标准结束符
private const string ScpiDelimiter = "\n"; private const string ScpiDelimiter = "\n";
/// <summary>
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化 IT7800E 交直流电源通信参数。
/// </summary>
public IT7800E(TcpConfig config) : base(config) public IT7800E(TcpConfig config) : base(config)
{ {
} }
#region 1. IEEE 488.2 #region 1. IEEE 488.2
/// <summary>
/// 清除状态命令。清除标准事件状态寄存器和错误队列
/// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*CLS{ScpiDelimiter}", ct); await SendAsync($"*CLS{ScpiDelimiter}", ct); //[cite: 1]
} }
/// <summary>
/// 读取电源识别字符串(制造商、产品型号、系统 SN、软件版本号
/// </summary>
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); //[cite: 1]
} }
/// <summary>
/// 复位命令。使电源恢复到出厂默认预定义安全值
/// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*RST{ScpiDelimiter}", ct); // await SendAsync($"*RST{ScpiDelimiter}", ct); //[cite: 1]
} }
/// <summary>
/// 读取状态字节寄存器
/// </summary>
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); //[cite: 1]
} }
#endregion #endregion
@@ -61,81 +77,57 @@ namespace DeviceCommand.Device
#region 2. #region 2.
/// <summary> /// <summary>
/// 设置电源输出源的电气模式/工作耦合模式 /// 设置电源输出源的电气模式/工作耦合模式 (AC, DC, ACDC, DCAC)[cite: 1]
/// (AC: 纯交流, DC: 纯直流, ACDC: 交直流混合模式)
/// </summary> /// </summary>
public virtual async Task (string , CancellationToken ct = default) public virtual async Task (PowerCouplingMode , CancellationToken ct = default)
{ {
string modeUpper = .ToUpper(); await SendAsync($":SOURce:FUNCtion {模式}{ScpiDelimiter}", ct); //[cite: 1]
if (modeUpper != "AC" && modeUpper != "DC" && modeUpper != "ACDC")
throw new ArgumentException("工作模式只能为 AC, DC, 或 ACDC");
await SendAsync($":SOURce:MODE {modeUpper}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 查询电源当前的工作模式 /// 查询电源当前的工作电气模式[cite: 1]
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":SOURce:MODE?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":SOURce:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 控制电源的主输出开关 (True: 开启输出, False: 关闭输出)
/// </summary>
public virtual async Task DC输出(bool , CancellationToken ct = default) public virtual async Task DC输出(bool , CancellationToken ct = default)
{ {
// 注意虽然方法名叫设置DC输出但对交直流电源它代表通用的主输出控制Main Output string = ? "ON" : "OFF"; //[cite: 1]
string = ? "ON" : "OFF"; await SendAsync($":OUTPut {参数}{ScpiDelimiter}", ct); //[cite: 1]
await SendAsync($":OUTPut {参数}{ScpiDelimiter}", ct);
} }
/// <summary>
/// 查询电源当前主输出开关状态 (返回 ON 或 OFF)
/// </summary>
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); return await WriteReadAsync($":OUTPut?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
#endregion #endregion
#region 3. (AC //DC ) #region 3. (AC //DC )
/// <summary>
/// 设定交流AC模式下的电压有效值 (单位: V)
/// </summary>
public virtual async Task (double , CancellationToken ct = default) public virtual async Task (double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:AC {0:F2}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage {0:F2}{1}", , ScpiDelimiter); //[cite: 1]
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary>
/// 设定直流DC模式或交直流(AC+DC)模式下的直流偏置电压值 (单位: V)
/// </summary>
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); string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:DC {0:F2}{1}", , ScpiDelimiter); //[cite: 1]
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary>
/// 设定电源交流输出的频率值 (单位: Hz)
/// </summary>
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); string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:FREQuency {0:F2}{1}", , ScpiDelimiter); //[cite: 1]
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary>
/// 设置硬件交流输出的有效值限流门限 (单位: A)
/// </summary>
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); //[cite: 1]
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -143,109 +135,127 @@ namespace DeviceCommand.Device
#region 4. #region 4.
/// <summary>
/// 查询通道实时测得的电压有效值RMS支持 AC 或 DC 模式下的回测)(单位: V)
/// </summary>
[Monitorable("交流电源电压")] [Monitorable("交流电源电压")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 查询通道实时测得的电流有效值RMS(单位: A)
/// </summary>
[Monitorable("交流电源电流")] [Monitorable("交流电源电流")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 查询通道实时测得的有功功率值 (单位: W)
/// </summary>
[Monitorable("交流电源功率")] [Monitorable("交流电源功率")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 查询通道实时测得的视在功率 (单位: VA)
/// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:POWer:APParent?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:POWer:APParent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 查询通道实时测得的输出频率 (单位: Hz)
/// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 查询当前的功率因数 (PF)
/// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:PF?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":MEASure:POWer:PFACtor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
#endregion #endregion
#region 5. #region 5.
/// <summary>
/// 设置电源的过流保护(OCP)值 (单位: A)
/// </summary>
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 {0:F3}{1}", , ScpiDelimiter); 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)值 (单位: V)
/// </summary>
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 {0:F2}{1}", , ScpiDelimiter); 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>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
} }
/// <summary>
/// 切换仪器至计算机控制的远程控制模式 (锁定前面板按键)
/// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct); await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1]
} }
/// <summary>
/// 切换仪器至前面板操作的本地模式 (前面板解锁)
/// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($":SYSTem:LOCal{ScpiDelimiter}", ct); await SendAsync($":SYSTem:LOCal{ScpiDelimiter}", ct); //[cite: 1]
}
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); //[cite: 1]
}
#endregion
#region 6.
/// <summary>
/// 清除最近的安时Ah与瓦时Wh统计数据[cite: 1]
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($"SENSe:AHOur:RESet{ScpiDelimiter}", ct); //[cite: 1]
await SendAsync($"SENSe:WHOur:RESet{ScpiDelimiter}", ct); //[cite: 1]
} }
/// <summary> /// <summary>
/// 清除仪器当前的保护状态(例如触发 OCP/OVP 被锁死后进行软件解锁) /// 读取当前充放电累计的安时值 (单位: Ah)[cite: 1]
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task<string> _Ah(CancellationToken ct = default)
{ {
await SendAsync($":PROTection:CLEar{ScpiDelimiter}", ct); return await WriteReadAsync($"FETCh:AHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
}
/// <summary>
/// 读取当前充放电累计的瓦时值 (单位: Wh)[cite: 1]
/// </summary>
public virtual async Task<string> _Wh(CancellationToken ct = default)
{
return await WriteReadAsync($"FETCh:WHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
}
/// <summary>
/// 读取充放电积分持续时间 (单位: 秒)[cite: 1]
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($"FETCh:ENERgy:TIME?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
}
/// <summary>
/// 切换设备的操作模式 (VOLTage: 电源模式, LOAD: 负载模式/放电, CURRent: 电流源)[cite: 1]
/// </summary>
public virtual async Task (DeviceOperationMode , CancellationToken ct = default)
{
await SendAsync($"SYSTem:OPERation:MODE {模式}{ScpiDelimiter}", ct); //[cite: 1]
}
/// <summary>
/// 设置放电截止电压/电压下限 (单位: V用于防止电池过放)[cite: 1]
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:LIMit:LOW {0:F2}{1}", , ScpiDelimiter); //[cite: 1]
await SendAsync(cmd, ct);
} }
#endregion #endregion

View File

@@ -6,9 +6,9 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] //[ADPCommand]
public class N36200 : Tcp public class N36200 : Tcp
{ {
//只有一个,八台产品一起用一个 //只有一个,八台产品一起用一个

View File

@@ -7,7 +7,7 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] [ADPCommand]
public class N36600 : Tcp public class N36600 : Tcp
@@ -229,26 +229,9 @@ namespace DeviceCommand.Device
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct); return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
} }
/// <summary>
/// 3.5.5 查询通道输出端子上测得的电池容量
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:CAP?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary> /// <summary>
/// 3.5.6 查询通道输出端子上测得的恒流输出时间长度 (单位: ms) /// 3.5.5 查询通道输出端子上测得的输出时间长度
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:TIM:CC?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
/// 3.5.7 查询通道输出端子上测得的输出时间长度
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
@@ -286,7 +269,7 @@ namespace DeviceCommand.Device
{ {
// 修正:去除前缀冒号 // 修正:去除前缀冒号
string = ? "ON" : "OFF"; string = ? "ON" : "OFF";
await SendAsync($"OUTP:TIM {状态}{SCPIDelimiter}", ct); await SendAsync($"OUTP:TIME {状态}{SCPIDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -295,7 +278,7 @@ namespace DeviceCommand.Device
public virtual async Task (double , CancellationToken ct = default) public virtual async Task (double , CancellationToken ct = default)
{ {
// 修正:去除前缀冒号 // 修正:去除前缀冒号
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIM:DATA {0:F1}{1}", , SCPIDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIME:DATA {0:F1}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -400,7 +383,14 @@ namespace DeviceCommand.Device
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>
/// 3.7.2.9 清除电压上限限制(恢复为设备物理允许的最大电压值)
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
// 发送 MAXimum 设为最大值以达到“清除限制”的效果
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>

View File

@@ -6,7 +6,7 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] [ADPCommand]
public class N69200 : Tcp public class N69200 : Tcp
@@ -24,15 +24,22 @@ namespace DeviceCommand.Device
#region 3.1. IEEE 488.2 #region 3.1. IEEE 488.2
/// <summary> /// <summary>
/// 清除状态命令。清除标准事件状态寄存器和错误队列 /// 清除状态命令。清除标准事件状态寄存器和错误队列 (*CLS)
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*CLS{ScpiDelimiter}", ct); await SendAsync($"*CLS{ScpiDelimiter}", ct);
} }
/// <summary>
/// 查询错误信息
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
}
/// <summary> /// <summary>
/// 读取电子负载设备识别字符串(制造商、产品型号、系统 SN、软件版本号 /// 读取电子负载设备识别字符串 (*IDN?)
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
@@ -40,7 +47,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 复位命令。使电子负载恢复到出厂默认配置状态 /// 复位命令。使电子负载恢复到出厂默认配置状态 (*RST)
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
@@ -48,7 +55,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 读取标准状态字节寄存器 /// 读取标准状态字节寄存器 (*STB?)
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
@@ -56,18 +63,24 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 保存当前系统参数到指定的存储单元 (单元范围依型号而定) /// 保存当前系统参数到指定的存储单元 (1~100)
/// </summary> /// </summary>
public virtual async Task (int , CancellationToken ct = default) public virtual async Task (int , CancellationToken ct = default)
{ {
if ( < 1 || > 100)
throw new ArgumentOutOfRangeException(nameof(), "存储单元位置应在 1~100 之间");
await SendAsync($"*SAV {单元}{ScpiDelimiter}", ct); await SendAsync($"*SAV {单元}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 调用指定存储单元中保存的系统参数 /// 调用指定存储单元中保存的系统参数 (1~100)
/// </summary> /// </summary>
public virtual async Task (int , CancellationToken ct = default) public virtual async Task (int , CancellationToken ct = default)
{ {
if ( < 1 || > 100)
throw new ArgumentOutOfRangeException(nameof(), "存储单元位置应在 1~100 之间");
await SendAsync($"*RCL {单元}{ScpiDelimiter}", ct); await SendAsync($"*RCL {单元}{ScpiDelimiter}", ct);
} }
@@ -76,34 +89,54 @@ namespace DeviceCommand.Device
#region 3.2. #region 3.2.
/// <summary> /// <summary>
/// 控制电子负载的输入控制开关(True: 开启拉载输入, False: 关闭拉载输入 /// 控制电子负载的输入控制开关(INPut:STATe <bool>
/// </summary> /// </summary>
public virtual async Task DC输入(bool , CancellationToken ct = default) public virtual async Task DC输入(bool , CancellationToken ct = default)
{ {
string = ? "ON" : "OFF"; string = ? "1" : "0";
await SendAsync($":INPut {参数}{ScpiDelimiter}", ct); await SendAsync($"INPut:STATe {参数}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 查询电子负载当前的拉载开关状态 (返回 ONOFF) /// 查询电子负载当前的拉载开关状态 (返回 01)
/// </summary> /// </summary>
public virtual async Task<string> DC输入状态(CancellationToken ct = default) public virtual async Task<string> DC输入状态(CancellationToken ct = default)
{ {
return await WriteReadAsync($":INPut?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"INPut:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 设置电子负载的基本工作模式 /// 设置电子负载的基本工作模式 (INPut:FUNCtion <mode>)
/// (CC: 恒电流, CV: 恒电压, CP: 恒功率, CR: 恒电阻) /// (支持 CC, CV, CR, CP, CCD, CVD, CRD, CPD, OCP, OPP, ESR, MPP, SWEEP 等)
/// </summary> /// </summary>
public virtual async Task (string , CancellationToken ct = default) public enum DeviceWorkMode
{ {
// 转换为大写以确保设备命令解析兼容 CC,
string modeUpper = .ToUpper(); CV,
if (modeUpper != "CC" && modeUpper != "CV" && modeUpper != "CP" && modeUpper != "CR") CR,
throw new ArgumentException("工作模式只能为 CC, CV, CP, 或 CR"); CP,
CCD,
await SendAsync($":MODE {modeUpper}{ScpiDelimiter}", ct); ESR,
AUTO,
DISCHARGE,
CHARGE,
OCP,
CVD,
CRD,
MPP,
CVCC,
CRCC,
CPCC,
CVCR,
WAVE,
SWEEP,
OPP,
CPD,
SZ
}
public virtual async Task (DeviceWorkMode , CancellationToken ct = default)
{
await SendAsync($"INPut:FUNCtion {Enum.GetName(模式)}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
@@ -111,79 +144,79 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MODE?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"INPut:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
#endregion #endregion
#region 3.3. (CC / CV / CP / CR) #region 3.3. (CC / CV / CP / CR )
/// <summary> /// <summary>
/// 设定恒电流模式(CC)下的拉载电流目标值 (单位: A) /// 设定恒电流模式(CC)大量程下的拉载电流目标值 (单位: A)
/// </summary> /// </summary>
public virtual async Task CC(double , CancellationToken ct = default) public virtual async Task CC(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":CURRent {0:F4}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CC:HIGH:LEVel {0:F4}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 查询恒电流模式下设定的拉载电流值 /// 查询恒电流模式大量程下设定的拉载电流值
/// </summary> /// </summary>
public virtual async Task<string> CC设定(CancellationToken ct = default) public virtual async Task<string> CC设定(CancellationToken ct = default)
{ {
return await WriteReadAsync($":CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"STATic:CC:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 设定恒电压模式(CV)下的拉载电压目标值 (单位: V) /// 设定恒电压模式(CV)大量程下的拉载电压目标值 (单位: V)
/// </summary> /// </summary>
public virtual async Task CV(double , CancellationToken ct = default) public virtual async Task CV(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":VOLTage {0:F3}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CV:HIGH:LEVel {0:F3}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 查询恒电压模式下设定的拉载电压值 /// 查询恒电压模式大量程下设定的拉载电压值
/// </summary> /// </summary>
public virtual async Task<string> CV设定(CancellationToken ct = default) public virtual async Task<string> CV设定(CancellationToken ct = default)
{ {
return await WriteReadAsync($":VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"STATic:CV:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 设定恒功率模式(CP)下的拉载功率目标值 (单位: W) /// 设定恒功率模式(CP)大量程下的拉载功率目标值 (单位: W)
/// </summary> /// </summary>
public virtual async Task CP(double , CancellationToken ct = default) public virtual async Task CP(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":POWer {0:F3}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CP:HIGH:LEVel {0:F3}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 查询恒功率模式下设定的拉载功率值 /// 查询恒功率模式大量程下设定的拉载功率值
/// </summary> /// </summary>
public virtual async Task<string> CP设定(CancellationToken ct = default) public virtual async Task<string> CP设定(CancellationToken ct = default)
{ {
return await WriteReadAsync($":POWer?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"STATic:CP:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 设定恒电阻模式(CR)下的等效拉载电阻阻值 (单位: Ω) /// 设定恒电阻模式(CR)大量程下的等效拉载电阻阻值 (单位: Ω)
/// </summary> /// </summary>
public virtual async Task CR(double , CancellationToken ct = default) public virtual async Task CR(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":RESistance {0:F4}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CR:HIGH:LEVel {0:F4}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 查询恒电阻模式下设定的拉载电阻值 /// 查询恒电阻模式大量程下设定的拉载电阻值
/// </summary> /// </summary>
public virtual async Task<string> CR设定(CancellationToken ct = default) public virtual async Task<string> CR设定(CancellationToken ct = default)
{ {
return await WriteReadAsync($":RESistance?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"STATic:CR:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
#endregion #endregion
@@ -196,7 +229,7 @@ namespace DeviceCommand.Device
[Monitorable("高压直流负载电压")] [Monitorable("高压直流负载电压")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
@@ -205,7 +238,7 @@ namespace DeviceCommand.Device
[Monitorable("高压直流负载电流")] [Monitorable("高压直流负载电流")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
@@ -214,15 +247,7 @@ namespace DeviceCommand.Device
[Monitorable("高压直流负载功率")] [Monitorable("高压直流负载功率")]
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
}
/// <summary>
/// 一键回读当前负载测得的电压、电流和功率的组合数组数据
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VAP?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
#endregion #endregion
@@ -230,29 +255,29 @@ namespace DeviceCommand.Device
#region 3.5. #region 3.5.
/// <summary> /// <summary>
/// 设置电子负载的过流保护(OCP)报警值 (单位: A) /// 设置电子负载的电流软保护阈值 (单位: A, 设置为 0 表示关闭)
/// </summary> /// </summary>
public virtual async Task _OCP(double , CancellationToken ct = default) public virtual async Task _OCP(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":PROTection:CURRent {0:F3}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:CURRent:PROTection {0:F3}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 设置电子负载的功率保护(OPP)报警值 (单位: W) /// 设置电子负载的功率保护值 (单位: W, 设置为 0 表示关闭)
/// </summary> /// </summary>
public virtual async Task _OPP(double , CancellationToken ct = default) public virtual async Task _OPP(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":PROTection:POWer {0:F3}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:POWer:PROTection {0:F3}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 设置电子负载的过压保护(OVP)报警值 (单位: V) /// 设置电子负载的电压软保护阈值 (单位: V, 设置为 0 表示关闭)
/// </summary> /// </summary>
public virtual async Task _OVP(double , CancellationToken ct = default) public virtual async Task _OVP(double , CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":PROTection:VOLTage {0:F3}{1}", , ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:VOLTage:PROTection {0:F3}{1}", , ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -261,37 +286,290 @@ namespace DeviceCommand.Device
#region 3.6. #region 3.6.
/// <summary> /// <summary>
/// 读取仪器的出错记录记录 (当系统产生告警或指令解析异常时读取) /// 读取负载报警/事件寄存器信息 (MEASure:EVENT?)
/// </summary> /// </summary>
public virtual async Task<string> (CancellationToken ct = default) public virtual async Task<string> (CancellationToken ct = default)
{ {
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); return await WriteReadAsync($"MEASure:EVENT?{ScpiDelimiter}", ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 切换电子负载到本地面板操作状态 (前面板按键解锁) /// 清除负载当前的保护触发锁定状态 (INPut:CLearPROTect)
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:LOCal{ScpiDelimiter}", ct);
}
/// <summary>
/// 切换电子负载到远程计算机操作状态 (前面板按键锁定)
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct);
}
/// <summary>
/// 清除负载当前的软件过流/过功率等保护(Protection)锁定触发状态
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($":PROTection:CLEar{ScpiDelimiter}", ct); await SendAsync($"INPut:CLearPROTect{ScpiDelimiter}", ct);
} }
#endregion #endregion
#region 3.7. 112~124
#region (COMBination:*)
public virtual async Task _CVCC(int , CancellationToken ct = default)
=> await SendAsync($"COMBination:CVCC:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CVCC(int , CancellationToken ct = default)
=> await SendAsync($"COMBination:CVCC:HIGH:RATE {速率}{ScpiDelimiter}", ct);
public virtual async Task _CVCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CVCC:HIGH:LEVel {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CVCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CVCC:CLIMit {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CRCC(int , CancellationToken ct = default)
=> await SendAsync($"COMBination:CRCC:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CRCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CRCC:LOW:LEVel {0:F4}{1}", , ScpiDelimiter), ct);
public virtual async Task _CRCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CRCC:CLIMit {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CRCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CRCC:LOW:SLEWRate:RAIse {0:F1}{1}", , ScpiDelimiter), ct);
public virtual async Task _CRCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CRCC:LOW:SLEWRate:FALL {0:F1}{1}", , ScpiDelimiter), ct);
public virtual async Task _CPCC(int , CancellationToken ct = default)
=> await SendAsync($"COMBination:CPCC:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CPCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CPCC:LOW:LEVel {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CPCC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CPCC:CLIMit {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CVCR(int , CancellationToken ct = default)
=> await SendAsync($"COMBination:CVCR:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CVCR(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CVCR:HIGH:LEVel {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CVCR(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "COMBination:CVCR:CLIMit {0:F4}{1}", , ScpiDelimiter), ct);
#endregion
#region (DYNAmic:*)
public virtual async Task _CC(int , CancellationToken ct = default)
=> await SendAsync($"DYNAmic:CC:MODe {模式}{ScpiDelimiter}", ct); // 0-连续, 1-单次, 2-A/B
public virtual async Task _CC(int , CancellationToken ct = default)
=> await SendAsync($"DYNAmic:CC:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:HIGH:LEVel:MAIN {0:F4}{1}", , ScpiDelimiter), ct);
public virtual async Task _CC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:HIGH:LEVel:TRANSient {0:F4}{1}", , ScpiDelimiter), ct);
public virtual async Task _CC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:HIGH:SLEWRate:RAIse {0:F1}{1}", , ScpiDelimiter), ct);
public virtual async Task _CC(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:HIGH:SLEWRate:FALL {0:F1}{1}", , ScpiDelimiter), ct);
public virtual async Task _CC(double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:WIDth:MAIN {0:F1}{1}", _ms, ScpiDelimiter), ct);
public virtual async Task _CC(double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CC:WIDth:TRANSient {0:F1}{1}", _ms, ScpiDelimiter), ct);
public virtual async Task _CV(int , CancellationToken ct = default)
=> await SendAsync($"DYNAmic:CV:MODe {模式}{ScpiDelimiter}", ct);
public virtual async Task _CV(int , CancellationToken ct = default)
=> await SendAsync($"DYNAmic:CV:RANGE {量程}{ScpiDelimiter}", ct);
public virtual async Task _CV(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CV:HIGH:LEVel:MAIN {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CV(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CV:HIGH:LEVel:TRANSient {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task _CV(double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CV:WIDth:MAIN {0:F1}{1}", _ms, ScpiDelimiter), ct);
public virtual async Task _CV(double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DYNAmic:CV:WIDth:TRANSient {0:F1}{1}", _ms, ScpiDelimiter), ct);
#endregion
#region (DISCHarge:* / CHARge:*)
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DISCHarge:CURRent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DISCHarge:VOLTage {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double _s, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DISCHarge:TIMe {0:F0}{1}", _s, ScpiDelimiter), ct);
public virtual async Task (double _Ah, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "DISCHarge:CAPacity {0:F3}{1}", _Ah, ScpiDelimiter), ct);
public virtual async Task<string> (CancellationToken ct = default)
=> await WriteReadAsync($"DISCHarge:ECHO:TIMe?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> (CancellationToken ct = default)
=> await WriteReadAsync($"DISCHarge:ECHo:CAPacity?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "CHARge:CURRent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "CHARge:VOLTage {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (int _s, CancellationToken ct = default)
=> await SendAsync($"CHARge:TIMe {时间_s}{ScpiDelimiter}", ct);
#endregion
#region OCP / OPP (OCP:* / OPP:*)
public virtual async Task OCP初始电流(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OCP:BCURrent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task OCP步进电流(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OCP:SCURrent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task OCP单步时间(double _s, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OCP:Time {0:F1}{1}", _s, ScpiDelimiter), ct);
public virtual async Task OCP终止电压(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OCP:EVOLtage {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task<string> OCP状态(CancellationToken ct = default)
=> await WriteReadAsync($"OCP:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> OCP结果电流(CancellationToken ct = default)
=> await WriteReadAsync($"OCP:RCURrent?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task OPP初始功率(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OPP:BPOWer {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task OPP步进功率(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OPP:SPOWer {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task OPP单步时间(double _s, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OPP:TIME {0:F1}{1}", _s, ScpiDelimiter), ct);
public virtual async Task OPP终止电压(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "OPP:EVOLtage {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task<string> OPP状态(CancellationToken ct = default)
=> await WriteReadAsync($"OPP:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> OPP结果功率(CancellationToken ct = default)
=> await WriteReadAsync($"OPP:RPOWer?{ScpiDelimiter}", ScpiDelimiter, ct);
#endregion
#region SWEEP WAVE (SWEEP:* / WAVE:*)
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:TCURrent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:MCURrent {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double _Hz, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:SFRequency {0:F1}{1}", _Hz, ScpiDelimiter), ct);
public virtual async Task (double _Hz, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:EFRequency {0:F1}{1}", _Hz, ScpiDelimiter), ct);
public virtual async Task (double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:STPTime {0:F1}{1}", _ms, ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "SWEEP:DUTYcycle {0:F1}{1}", , ScpiDelimiter), ct);
public virtual async Task<string> (CancellationToken ct = default)
=> await WriteReadAsync($"SWEEP:CFRequency?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> (CancellationToken ct = default)
=> await WriteReadAsync($"SWEEP:RESult?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task (int , CancellationToken ct = default)
=> await SendAsync($"WAVE:RANGe {量程}{ScpiDelimiter}", ct);
public virtual async Task (int , CancellationToken ct = default)
=> await SendAsync($"WAVE:TYPe {类型}{ScpiDelimiter}", ct); // 0-正弦波, 1-方波, 2-三角波, 3-锯齿波, 4-自定义
public virtual async Task (double _Hz, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "WAVE:FREQuency {0:F1}{1}", _Hz, ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "WAVE:SETValue {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task (double _ms, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "WAVE:TIMe {0:F1}{1}", _ms, ScpiDelimiter), ct);
public virtual async Task (double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "WAVE:BVALue {0:F3}{1}", , ScpiDelimiter), ct);
#endregion
#region ESR (ESR:*)
public virtual async Task ESR带载量程(int , CancellationToken ct = default)
=> await SendAsync($"ESR:LOADRange {量程}{ScpiDelimiter}", ct);
public virtual async Task ESR大量程带载电流(double , CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "ESR:CURRenthigh {0:F3}{1}", , ScpiDelimiter), ct);
public virtual async Task ESR测量方法(int , CancellationToken ct = default)
=> await SendAsync($"ESR:MODE {方法}{ScpiDelimiter}", ct); // 0-方波平均法, 1-单脉冲法
public virtual async Task ESR采样量程(int , CancellationToken ct = default)
=> await SendAsync($"ESR:MEASurerange {量程}{ScpiDelimiter}", ct); // 0-1000mV, 1-100mV, 2-10mV
public virtual async Task<string> ESR状态(CancellationToken ct = default)
=> await WriteReadAsync($"ESR:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> ESR结果(CancellationToken ct = default)
=> await WriteReadAsync($"ESR:RESult?{ScpiDelimiter}", ScpiDelimiter, ct);
#endregion
#region MPPT (MPP:*)
public virtual async Task MPPT步进电压(double _V, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "MPP:BVOLtage {0:F3}{1}", _V, ScpiDelimiter), ct);
public virtual async Task MPPT步进时间(double _s, CancellationToken ct = default)
=> await SendAsync(string.Format(CultureInfo.InvariantCulture, "MPP:TIME {0:F1}{1}", _s, ScpiDelimiter), ct);
public virtual async Task MPPT模式(int , CancellationToken ct = default)
=> await SendAsync($"MPP:MODE {模式}{ScpiDelimiter}", ct); // 0-扫描模式, 1-跟踪模式
public virtual async Task<string> MPPT状态(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:STATus?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> MPPT最大功率(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:MPOWer?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> MPPT最大功率电压(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:MVOLtage?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> MPPT最大功率电流(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:MCURrent?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> MPPT开路电压(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:OVOLtage?{ScpiDelimiter}", ScpiDelimiter, ct);
public virtual async Task<string> MPPT短路电流(CancellationToken ct = default)
=> await WriteReadAsync($"MPP:SCURrent?{ScpiDelimiter}", ScpiDelimiter, ct);
#endregion
#endregion
} }
} }

View File

@@ -3,20 +3,20 @@ using DeviceCommand.Base;
using Model.Models; using Model.Models;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] [ADPCommand]
public class SDS2000X_HD : Tcp public class SDS2000X_HD : Tcp
{ {
// 示波器底层 Socket 字符串命令通常以换行符 \n 结束 // 示波器底层 Socket 字符串命令以换行符 \n 结束
private const string ScpiDelimiter = "\n"; private const string ScpiDelimiter = "\n";
/// <summary> /// <summary>
/// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化示波器通信参数。 /// 构造函数:传入 <see cref="TcpConfig"/> 一次性初始化示波器通信参数。
/// 鼎阳示波器网口 Socket 默认端口通常为 5025请在配置中设置。
/// </summary> /// </summary>
public SDS2000X_HD(TcpConfig config) : base(config) public SDS2000X_HD(TcpConfig config) : base(config)
{ {
@@ -31,7 +31,13 @@ namespace DeviceCommand.Device
{ {
await SendAsync($"*CLS{ScpiDelimiter}", ct); await SendAsync($"*CLS{ScpiDelimiter}", ct);
} }
/// <summary>
/// 查询错误信息
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
}
/// <summary> /// <summary>
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本) /// 读取示波器识别字符串(制造商、型号、序列号、固件版本)
/// </summary> /// </summary>
@@ -49,8 +55,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 【补充】查询先前操作是否完成。 /// 查询先前操作是否完成。
/// 在重置设备(*RST)或切换大物理量程后调用,返回 "1" 代表示波器继电器切换就绪,防止后续指令引发阻塞。
/// </summary> /// </summary>
public virtual async Task<bool> _OPC(CancellationToken ct = default) public virtual async Task<bool> _OPC(CancellationToken ct = default)
{ {
@@ -63,65 +68,82 @@ namespace DeviceCommand.Device
#region 2. (Run / Stop / Single) #region 2. (Run / Stop / Single)
/// <summary> /// <summary>
/// 控制示波器开始捕获波形 (等同于按下前端面板的 Run 键) /// 控制示波器开始捕获波形
/// </summary> /// </summary>
public virtual async Task _RUN(CancellationToken ct = default) public virtual async Task _RUN(CancellationToken ct = default)
{ {
await SendAsync($"RUN{ScpiDelimiter}", ct); await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 停止捕获波形 (等同于按下前端面板的 Stop 键) /// 停止捕获波形
/// </summary> /// </summary>
public virtual async Task _STOP(CancellationToken ct = default) public virtual async Task _STOP(CancellationToken ct = default)
{ {
await SendAsync($"STOP{ScpiDelimiter}", ct); await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 强制示波器进入单次触发捕获模式 (常用于捕捉充电瞬间的过冲浪涌波形) /// 强制示波器进入单次触发捕获模式
/// </summary> /// </summary>
public virtual async Task _SINGLE(CancellationToken ct = default) public virtual async Task _SINGLE(CancellationToken ct = default)
{ {
await SendAsync($"SINGle{ScpiDelimiter}", ct); await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 触发一次波形采样 (当触发源设为 Manual 时使用) /// 触发一次波形采样
/// </summary> /// </summary>
public virtual async Task (CancellationToken ct = default) public virtual async Task (CancellationToken ct = default)
{ {
await SendAsync($"*TRG{ScpiDelimiter}", ct); await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
} }
/// <summary>
/// 示波器触发控制模式(符合 SDS2000X-HD 规范)。
/// </summary>
public enum TriggerMode
{
/// <summary>
/// 自动触发模式 (即使无触发信号也周期性刷屏)
/// </summary>
AUTO,
/// <summary> /// <summary>
/// 【补充】设置触发模式 (AUTO, NORM, SINGLE) /// 普通触发模式 (仅当满足触发条件时才刷新)
/// </summary> /// </summary>
public virtual async Task (string mode, CancellationToken ct = default) NORM,
{
string modeUpper = mode.ToUpper();
if (modeUpper != "AUTO" && modeUpper != "NORM" && modeUpper != "SINGLE")
throw new ArgumentException("触发模式只能为 AUTO, NORM, 或 SINGLE");
await SendAsync($"TRMD {modeUpper}{ScpiDelimiter}", ct); /// <summary>
/// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)
/// </summary>
SINGLE
}
/// <summary>
/// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次)
/// </summary>
/// <param name="mode">触发模式枚举</param>
/// <param name="ct">取消令牌</param>
public virtual async Task (TriggerMode mode, CancellationToken ct = default)
{
// 例如发送TRMD AUTO\n、TRMD NORM\n 或 TRMD SINGLE\n
string cmd = $"TRMD {mode}{ScpiDelimiter}";
await SendAsync(cmd, ct);
} }
#endregion #endregion
#region 3. Channel (C1 ~ C4) #region 3. Channel (C1 ~ C4)
/// <summary> /// <summary>
/// 开启或关闭指定的模拟通道 /// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
/// </summary> /// </summary>
public virtual async Task (int channel, bool enable, CancellationToken ct = default) public virtual async Task (int channel, bool enable, CancellationToken ct = default)
{ {
// 修正:根据手册规范,使用 1/0 比 ON/OFF 在高低版本固件中兼容性更稳定 string state = enable ? "ON" : "OFF";
string state = enable ? "1" : "0";
await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct); await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
} }
/// <summary> /// <summary>
/// 设置指定通道的垂直电压档位 (Volts/Div,单位: V例如 0.05 代表 50mV/div) /// 设置指定通道的垂直电压档位 (Volts/Div)
/// </summary> /// </summary>
public virtual async Task (int channel, double volts, CancellationToken ct = default) public virtual async Task (int channel, double volts, CancellationToken ct = default)
{ {
@@ -130,7 +152,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 【补充验证】查询指定通道当前的电压档位 (用于 Setup-Verify 闭环验证逻辑) /// 查询指定通道当前的电压档位
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
@@ -138,22 +160,29 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 设置指定通道的垂直偏移量 (Offset,单位: V) /// 设置指定通道的垂直偏移量 (Offset)
/// </summary> /// </summary>
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);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 设置通道输入阻抗与耦合模式 /// 示波器通道输入阻抗类型。
/// </summary> /// </summary>
/// <param name="coupling">合法参数A1M (交流1M), D1M (直流1M), D50 (直流50欧)</param> public enum ImpedanceType
public virtual async Task (int channel, string coupling, CancellationToken ct = default)
{ {
string coupUpper = coupling.ToUpper(); /// <summary>50Ω 阻抗</summary>
await SendAsync($"C{channel}:COUPling {coupUpper}{ScpiDelimiter}", ct); FIFty,
/// <summary>1MΩ 阻抗</summary>
ONEM
}
public virtual async Task (int channel, ImpedanceType impedance, CancellationToken ct = default)
{
// 例如发送C1:IMPedance FIFty\n 或 C1:IMPedance ONEM\n
string cmd = $"CHANnel{channel}:IMPedance {impedance}{ScpiDelimiter}";
await SendAsync(cmd, ct);
} }
#endregion #endregion
@@ -161,21 +190,19 @@ namespace DeviceCommand.Device
#region 4. Timebase #region 4. Timebase
/// <summary> /// <summary>
/// 设置示波器的水平时基档位 (Time/Div,单位: s例如 0.001 代表 1ms/div) /// 设置示波器的水平时基档位 (Time/Div)
/// </summary> /// </summary>
public virtual async Task (double scale, CancellationToken ct = default) public virtual async Task (double scale, CancellationToken ct = default)
{ {
// 修正:统一使用更精简且符合手册定义的简写形式 TDIV
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 设置示波器的触发水平延迟位置 (Horizontal Delay,单位: s) /// 设置示波器的触发水平延迟位置 (Horizontal Delay)
/// </summary> /// </summary>
public virtual async Task (double delay, CancellationToken ct = default) public virtual async Task (double delay, CancellationToken ct = default)
{ {
// 修正:采用手册标准简写 TRDL 效率更高
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -185,12 +212,12 @@ namespace DeviceCommand.Device
#region 5. Trigger #region 5. Trigger
/// <summary> /// <summary>
/// 设置边沿触发的电平值 (Trigger Level,单位: V) /// 设置边沿触发的电平值 (Trigger Level)
/// </summary> /// </summary>
public virtual async Task (double level, CancellationToken ct = default) public virtual async Task (double level, CancellationToken ct = default)
{ {
// 修正:采用标准简写 TRLV
string cmd = string.Format(CultureInfo.InvariantCulture, "TRLV {0:F3}{1}", level, ScpiDelimiter); string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
@@ -199,30 +226,24 @@ namespace DeviceCommand.Device
/// </summary> /// </summary>
public virtual async Task (string source, CancellationToken ct = default) public virtual async Task (string source, CancellationToken ct = default)
{ {
// 修正:采用标准简写 TRSE 体系配置指令 await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct);
await SendAsync($"TRIGger:SOURce {source.ToUpper()}{ScpiDelimiter}", ct);
} }
#endregion #endregion
#region 6. Measure () #region 6. Measure
/// <summary> /// <summary>
/// 查询指定通道自动测量项的当前实时测量数值 /// 查询指定通道自动测量项的当前实时测量数值
/// </summary> /// </summary>
/// <param name="channel">通道号 (1-4)</param>
/// <param name="paramName">参数名称助记符:
/// PKPK(峰峰值), MAX(最大值), MIN(最小值), AMPL(振幅值),
/// FREQ(频率), PER(周期), MEAN(平均值), RMS(均方根) 等</param>
public virtual async Task<string> (int channel, string paramName, CancellationToken ct = default) public virtual async Task<string> (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); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 轮询便捷接口:查询指定通道的电压峰峰值 (Vpp) /// 查询指定通道的电压峰峰值 (Vpp)
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
@@ -230,7 +251,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 轮询便捷接口:查询指定通道的频率值 (Frequency) /// 查询指定通道的频率值 (Frequency)
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
@@ -238,7 +259,7 @@ namespace DeviceCommand.Device
} }
/// <summary> /// <summary>
/// 轮询便捷接口:查询指定通道的真均方根电压值 (Vrms) /// 查询指定通道的真均方根电压值 (Vrms)
/// </summary> /// </summary>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
@@ -246,5 +267,60 @@ namespace DeviceCommand.Device
} }
#endregion #endregion
#region 7.
/// 【一键获取并保存截图】
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
/// </summary>
/// <param name="saveFilePath">本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png")</param>
/// <param name="ct">取消令牌</param>
/// <returns>返回是否获取并保存成功</returns>
public virtual async Task<bool> (string saveFilePath, CancellationToken ct = default)
{
try
{
// 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync
// 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
// 2. 校验返回数据
if (pureImageBytes == null || pureImageBytes.Length < 8)
{
System.Diagnostics.Debug.WriteLine("错误:接收到的图片数据为空或长度不足。");
return false;
}
// 3. 核心校验:验证是否为标准的 PNG 文件格式 (PNG 头通常为 89 50 4E 47)
if (pureImageBytes[0] != 0x89 || pureImageBytes[1] != 'P' || pureImageBytes[2] != 'N' || pureImageBytes[3] != 'G')
{
System.Diagnostics.Debug.WriteLine("错误:接收到的二进制数据不符合标准 PNG 格式头!");
return false;
}
// 4. 如果传入的目标文件夹路径不存在,自动帮其创建
string directory = Path.GetDirectoryName(saveFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// 5. 使用文件流直接将 PNG 字节数据落地写入本地硬盘
using (FileStream fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
{
await fs.WriteAsync(pureImageBytes, 0, pureImageBytes.Length, ct);
await fs.FlushAsync(ct); // 强制刷新,确保数据完整落地
}
System.Diagnostics.Debug.WriteLine($"成功:截图已保存至路径: {saveFilePath}");
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"崩溃:保存截图时发生未知异常: {ex.Message}");
return false;
}
}
#endregion
} }
} }

View File

@@ -6,7 +6,7 @@ using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace DeviceCommand.Device namespace DeviceCommand.Devices
{ {
[ADPCommand] [ADPCommand]
public class SPAW7000 : Tcp public class SPAW7000 : Tcp
@@ -31,7 +31,14 @@ namespace DeviceCommand.Device
{ {
await SendAsync($"*CLS{ScpiDelimiter}", ct); await SendAsync($"*CLS{ScpiDelimiter}", ct);
} }
/// <summary>
/// 查询错误信息 发hsl居然可以但是手册上没有而且软件上也用不了这个命令
/// </summary>
//public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
//{
// string query = string.Format(":SYSTem:ERRor?", ScpiDelimiter);
// return await WriteReadAsync(query, ScpiDelimiter, ct);
//}
/// <summary> /// <summary>
/// 读取功率分析仪识别字符串(制造商、产品型号、系统 SN、软件版本号 /// 读取功率分析仪识别字符串(制造商、产品型号、系统 SN、软件版本号
/// </summary> /// </summary>
@@ -58,108 +65,142 @@ namespace DeviceCommand.Device
#endregion #endregion
#region 2. MEASure / NUMeric () #region 2. NUMeric ()
/// <summary> /// <summary>
/// 查询指定通道的实时 RMS 电压值 (单位: V) /// 查询指定通道的实时 RMS 电压值 (单位: V)
/// </summary> /// </summary>
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? U,{0}{1}", channel, ScpiDelimiter); // 1. 先配置 ITEM1 为指定通道的电压有效值 (URMS)
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 URMS,{0}{1}", channel, ScpiDelimiter);
await SendAsync(setItem, ct);
// 2. 再读取 ITEM1 的值
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
return await WriteReadAsync(query, ScpiDelimiter, ct); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 查询指定通道的实时 RMS 电流值 (单位: A) /// 查询指定通道的实时 RMS 电流值 (单位: A)
/// </summary> /// </summary>
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? I,{0}{1}", channel, ScpiDelimiter); // 1. 配置 ITEM1 为指定通道的电流有效值 (IRMS)
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 IRMS,{0}{1}", channel, ScpiDelimiter);
await SendAsync(setItem, ct);
// 2. 读取 ITEM1 的值
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
return await WriteReadAsync(query, ScpiDelimiter, ct); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 查询指定通道的实时有功功率值 (单位: W) /// 查询指定通道的实时有功功率值 (单位: W)
/// </summary> /// </summary>
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? P,{0}{1}", channel, ScpiDelimiter); // 1. 配置 ITEM1 为指定通道的有功功率 (P)
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 P,{0}{1}", channel, ScpiDelimiter);
await SendAsync(setItem, ct);
// 2. 读取 ITEM1 的值
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
return await WriteReadAsync(query, ScpiDelimiter, ct); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 查询指定通道的实时频率值 (单位: Hz) /// 查询指定通道的实时频率值 (单位: Hz)
/// </summary> /// </summary>
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? FREQuency,{0}{1}", channel, ScpiDelimiter); // 1. 配置 ITEM1 为指定通道的电压频率 (FU)
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 FU,{0}{1}", channel, ScpiDelimiter);
await SendAsync(setItem, ct);
// 2. 读取 ITEM1 的值
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
return await WriteReadAsync(query, ScpiDelimiter, ct); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
/// <summary> /// <summary>
/// 查询指定通道的功率因数 (Power Factor) /// 查询指定通道的功率因数 (Power Factor)
/// </summary> /// </summary>
/// <param name="channel">通道号 (例如: 1, 2, 3...)</param>
public virtual async Task<string> (int channel, CancellationToken ct = default) public virtual async Task<string> (int channel, CancellationToken ct = default)
{ {
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? PF,{0}{1}", channel, ScpiDelimiter); // 1. 配置 ITEM1 为指定通道的功率因数 (LAMBda)
return await WriteReadAsync(query, ScpiDelimiter, ct); string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 LAMBda,{0}{1}", channel, ScpiDelimiter);
} await SendAsync(setItem, ct);
/// <summary> // 2. 读取 ITEM1 的值
/// 自定义组合参数批量读取接口 string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
/// </summary>
/// <param name="parameter">参数助记符 (如 "U,I,P" 或 "S,Q,LAMBda")</param>
/// <param name="channel">通道号</param>
public virtual async Task<string> (string parameter, int channel, CancellationToken ct = default)
{
string query = string.Format(CultureInfo.InvariantCulture, ":MEASure:NUMeric:VALue? {0},{1}{2}", parameter, channel, ScpiDelimiter);
return await WriteReadAsync(query, ScpiDelimiter, ct); return await WriteReadAsync(query, ScpiDelimiter, ct);
} }
#endregion #endregion
#region 3. INPut #region 3. INPut
/// <summary>
/// SPAW7000 电压量程枚举 (单位: V)
/// </summary>
public enum VoltageRange
{
V_15 = 15,
V_30 = 30,
V_60 = 60,
V_100 = 100,
V_150 = 150,
V_300 = 300,
V_600 = 600,
V_1000 = 1000
}
/// <summary> /// <summary>
/// 设置指定通道的电压量程 (例如: 15, 30, 60, 150, 300, 600, 1000) /// SPAW7000 电流量程枚举 (单位: A)
/// </summary> /// </summary>
public virtual async Task (int channel, double range, CancellationToken ct = default) public enum CurrentRange
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:VOLTage:RANGe {0},{1:F1}{2}", channel, range, ScpiDelimiter); A_1 = 1,
A_2 = 2,
A_5 = 5,
A_10 = 10,
A_20 = 20,
A_50 = 50
}
/// <summary>
/// 设置指定通道的电压量程
/// </summary>
/// <param name="channel">通道号/单元号 (1 - 7)</param>
/// <param name="range">电压量程枚举</param>
/// <param name="ct">取消令牌</param>
public virtual async Task (int channel, VoltageRange range, CancellationToken ct = default)
{
// 正确格式例如: :INPut:VOLTage:RANGe:ELEMent1 300\n
string cmd = string.Format(CultureInfo.InvariantCulture,
":INPut:VOLTage:RANGe:ELEMent{0} {1}{2}",
channel, (int)range, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary> /// <summary>
/// 设置指定通道的电流量程 (取决于接线单元或传感器输入类型) /// 设置指定通道的电流量程
/// </summary> /// </summary>
public virtual async Task (int channel, double range, CancellationToken ct = default) /// <param name="channel">通道号/单元号 (1 - 7)</param>
/// <param name="range">电流量程枚举</param>
/// <param name="ct">取消令牌</param>
public virtual async Task (int channel, CurrentRange range, CancellationToken ct = default)
{ {
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:CURRent:RANGe {0},{1:F3}{2}", channel, range, ScpiDelimiter); // 正确格式例如: :INPut:CURRent:RANGe:ELEMent1 5\n
string cmd = string.Format(CultureInfo.InvariantCulture,
":INPut:CURRent:RANGe:ELEMent{0} {1}{2}",
channel, (int)range, ScpiDelimiter);
await SendAsync(cmd, ct); await SendAsync(cmd, ct);
} }
/// <summary>
/// 设置通道的耦合模式 (AC, DC, ACDC)
/// </summary>
public virtual async Task (int channel, string mode, CancellationToken ct = default)
{
string modeUpper = mode.ToUpper();
if (modeUpper != "AC" && modeUpper != "DC" && modeUpper != "ACDC")
throw new ArgumentException("耦合模式只能为 AC, DC, 或 ACDC");
string cmd = string.Format(CultureInfo.InvariantCulture, ":INPut:COUPling {0},{1}{2}", channel, modeUpper, ScpiDelimiter);
await SendAsync(cmd, ct);
}
#endregion #endregion
#region 4. SYSTem #region 4. SYSTem
/// <summary> /// <summary>
/// 14. 查询仪器型号名称 /// 14. 查询仪器型号名称
/// </summary> /// </summary>

View File

@@ -0,0 +1,193 @@
using Common.Attributes;
using NModbus;
using NModbus.Serial;
using System;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Flexible
{
[ADPCommand]
public static class FModbusRTU
{
private static readonly SemaphoreSlim _commLock = new(1, 1);
private static SerialPort CreatePort(
string portName,
int baudRate,
int dataBits,
StopBits stopBits,
Parity parity,
int readTimeout,
int writeTimeout)
{
return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = readTimeout > 0 ? readTimeout : SerialPort.InfiniteTimeout,
WriteTimeout = writeTimeout > 0 ? writeTimeout : SerialPort.InfiniteTimeout
};
}
private static IModbusMaster CreateMaster(SerialPort port)
{
return new ModbusFactory().CreateRtuMaster(port);
}
#region Holding Register
public static async Task<ushort[]> ReadHoldingRegistersAsync(
string portName,
int baudRate,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (readTimeout > 0)
cts.CancelAfter(readTimeout);
return await master
.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
}
finally
{
_commLock.Release();
}
}
public static async Task WriteSingleRegisterAsync(
string portName,
int baudRate,
byte slaveAddress,
ushort registerAddress,
ushort value,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (writeTimeout > 0)
cts.CancelAfter(writeTimeout);
await master
.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(writeTimeout),cts.Token);
}
finally
{
_commLock.Release();
}
}
#endregion
#region Coil
public static async Task<bool[]> ReadCoilsAsync(
string portName,
int baudRate,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (readTimeout > 0)
cts.CancelAfter(readTimeout);
return await master
.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
}
finally
{
_commLock.Release();
}
}
public static async Task WriteSingleCoilAsync(
string portName,
int baudRate,
byte slaveAddress,
ushort coilAddress,
bool value,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);
port.Open();
var master = CreateMaster(port);
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (writeTimeout > 0)
cts.CancelAfter(writeTimeout);
await master
.WriteSingleCoilAsync(slaveAddress, coilAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(writeTimeout), cts.Token);
}
finally
{
_commLock.Release();
}
}
#endregion
}
}

View File

@@ -0,0 +1,133 @@
using Common.Attributes;
using NModbus;
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Flexible
{
[ADPCommand]
public static class FModbusTCP
{
private static readonly SemaphoreSlim _commLock = new(1, 1);
private static async Task<IModbusMaster> ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct)
{
var tcpClient = new TcpClient();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (sendTimeout > 0)
cts.CancelAfter(sendTimeout);
await tcpClient.ConnectAsync(ipAddress, port, cts.Token);
return new ModbusFactory().CreateMaster(tcpClient);
}
#region Holding Registers
public static async Task<ushort[]> ReadHoldingRegistersAsync(
string ipAddress,
int port,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int sendTimeout = 3000,
int receiveTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
var result = await ((IModbusMaster)master)
.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(receiveTimeout), ct);
return result;
}
finally
{
_commLock.Release();
}
}
public static async Task WriteSingleRegisterAsync(
string ipAddress,
int port,
byte slaveAddress,
ushort registerAddress,
ushort value,
int sendTimeout = 3000,
int receiveTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
await ((IModbusMaster)master)
.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(sendTimeout),ct);
}
finally
{
_commLock.Release();
}
}
#endregion
#region Coils
public static async Task<bool[]> ReadCoilsAsync(
string ipAddress,
int port,
byte slaveAddress,
ushort startAddress,
ushort numberOfPoints,
int sendTimeout = 3000,
int receiveTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
var result = await ((IModbusMaster)master)
.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
.WaitAsync(TimeSpan.FromMilliseconds(receiveTimeout), ct);
return result;
}
finally
{
_commLock.Release();
}
}
public static async Task WriteSingleCoilAsync(
string ipAddress,
int port,
byte slaveAddress,
ushort coilAddress,
bool value,
int sendTimeout = 3000,
int receiveTimeout = 3000,
CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
await ((IModbusMaster)master)
.WriteSingleCoilAsync(slaveAddress, coilAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(sendTimeout),ct);
}
finally
{
_commLock.Release();
}
}
#endregion
}
}

View File

@@ -0,0 +1,104 @@
using Common.Attributes;
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Flexible
{
[ADPCommand]
public static class FSerialPort
{
private static readonly SemaphoreSlim _commLock = new(1, 1);
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)
{
Encoding = Encoding.UTF8,
WriteTimeout = sendTimeout > 0 ? sendTimeout : SerialPort.InfiniteTimeout,
ReadTimeout = receiveTimeout > 0 ? receiveTimeout : SerialPort.InfiniteTimeout
};
}
#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)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, parity, dataBits, stopBits, sendTimeout, receiveTimeout);
port.Open();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (sendTimeout > 0)
cts.CancelAfter(sendTimeout);
var bytes = port.Encoding.GetBytes(command);
await port.BaseStream.WriteAsync(bytes, 0, bytes.Length, cts.Token);
await port.BaseStream.FlushAsync(cts.Token);
}
finally
{
_commLock.Release();
}
}
#endregion
#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)
{
await _commLock.WaitAsync(ct);
try
{
using var port = CreatePort(
portName, baudRate, parity, dataBits, stopBits, sendTimeout, receiveTimeout);
port.Open();
// Send
var sendBytes = port.Encoding.GetBytes(command);
await port.BaseStream.WriteAsync(sendBytes, 0, sendBytes.Length, ct);
await port.BaseStream.FlushAsync(ct);
// Read
delimiter ??= "\n";
var sb = new StringBuilder();
byte[] buffer = new byte[256];
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (receiveTimeout > 0)
cts.CancelAfter(receiveTimeout);
while (!cts.Token.IsCancellationRequested)
{
int read = await port.BaseStream.ReadAsync(
buffer, 0, buffer.Length, cts.Token);
if (read == 0)
break;
sb.Append(port.Encoding.GetString(buffer, 0, read));
int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
return sb.ToString(0, index).Trim();
}
throw new TimeoutException("串口读取超时");
}
finally
{
_commLock.Release();
}
}
#endregion
}
}

View File

@@ -0,0 +1,115 @@
using Common.Attributes;
using System.Net.Sockets;
using System.Text;
namespace DeviceCommand.Flexible
{
[ADPCommand]
public static class FTCP
{
private static readonly SemaphoreSlim _commLock = new(1, 1);
#region Send
public static async Task SendAsync(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var client = new TcpClient();
client.SendTimeout = sendTimeout;
await client.ConnectAsync(ipAddress, port, ct);
using NetworkStream stream = client.GetStream();
await stream.WriteAsync(buffer, 0, buffer.Length, ct).WaitAsync(TimeSpan.FromMilliseconds(sendTimeout));
}
finally
{
_commLock.Release();
}
}
public static Task SendAsync(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default)
{
return SendAsync( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct);
}
#endregion
#region Read
public static async Task<byte[]> ReadAsync(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var client = new TcpClient();
client.ReceiveTimeout = receiveTimeout;
await client.ConnectAsync(ipAddress, port, ct);
using NetworkStream stream = client.GetStream();
byte[] buffer = new byte[length];
int offset = 0;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (receiveTimeout > 0)
cts.CancelAfter(receiveTimeout);
while (offset < length)
{
int read = await stream.ReadAsync(buffer, offset, length - offset, cts.Token);
if (read == 0) break;
offset += read;
}
return buffer[..offset];
}
finally
{
_commLock.Release();
}
}
public static async Task<string> ReadLineAsync( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
using var client = new TcpClient();
client.ReceiveTimeout = receiveTimeout;
await client.ConnectAsync(ipAddress, port, ct);
using NetworkStream stream = client.GetStream();
var sb = new StringBuilder();
byte[] buffer = new byte[1024];
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (receiveTimeout > 0)
cts.CancelAfter(receiveTimeout);
while (!cts.Token.IsCancellationRequested)
{
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token);
if (bytesRead == 0) break;
sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal);
if (index >= 0)
return sb.ToString(0, index).Trim();
}
throw new TimeoutException("读取超时或对端关闭");
}
finally
{
_commLock.Release();
}
}
#endregion
}
}

View File

@@ -21,6 +21,12 @@ namespace DeviceEditModule
containerRegistry.RegisterForNavigation<N69200View>("N69200View"); containerRegistry.RegisterForNavigation<N69200View>("N69200View");
containerRegistry.RegisterForNavigation<SDS2000X_HDView>("SDS2000X_HDView"); containerRegistry.RegisterForNavigation<SDS2000X_HDView>("SDS2000X_HDView");
containerRegistry.RegisterForNavigation<SPAW7000View>("SPAW7000View"); containerRegistry.RegisterForNavigation<SPAW7000View>("SPAW7000View");
containerRegistry.Register<IT7800EViewModel>();
containerRegistry.Register<N36200ViewModel>();
containerRegistry.Register<N36600ViewModel>();
containerRegistry.Register<N69200ViewModel>();
containerRegistry.Register<SDS2000X_HDViewModel>();
containerRegistry.Register<SPAW7000ViewModel>();
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;
@@ -92,9 +92,9 @@ namespace DeviceEditModule.ViewModels
set => SetProperty(ref _currentLimit, value); set => SetProperty(ref _currentLimit, value);
} }
private string _selectedMode = "AC"; private PowerCouplingMode _selectedMode = PowerCouplingMode.AC;
/// <summary>待设置的电源工作模式AC / DC / ACDC。</summary> /// <summary>待设置的电源工作模式AC / DC / ACDC。</summary>
public string SelectedMode public PowerCouplingMode SelectedMode
{ {
get => _selectedMode; get => _selectedMode;
set => SetProperty(ref _selectedMode, value); set => SetProperty(ref _selectedMode, value);

View File

@@ -1,4 +1,4 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;

View File

@@ -1,4 +1,4 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;

View File

@@ -1,4 +1,4 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;
@@ -6,6 +6,7 @@ using System.Threading;
using System.Windows.Input; using System.Windows.Input;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
using static DeviceCommand.Devices.N69200;
namespace DeviceEditModule.ViewModels namespace DeviceEditModule.ViewModels
{ {
@@ -49,9 +50,9 @@ namespace DeviceEditModule.ViewModels
#region #region
private string _selectedMode = "CC"; private DeviceWorkMode _selectedMode = DeviceWorkMode.CC;
/// <summary>工作模式CC / CV / CP / CR。</summary> /// <summary>工作模式CC / CV / CP / CR。</summary>
public string SelectedMode public DeviceWorkMode SelectedMode
{ {
get => _selectedMode; get => _selectedMode;
set => SetProperty(ref _selectedMode, value); set => SetProperty(ref _selectedMode, value);
@@ -155,8 +156,7 @@ namespace DeviceEditModule.ViewModels
public ICommand SetOcpCommand { get; } public ICommand SetOcpCommand { get; }
public ICommand SetOppCommand { get; } public ICommand SetOppCommand { get; }
public ICommand ClearAlarmCommand { get; } public ICommand ClearAlarmCommand { get; }
public ICommand SetRemoteModeCommand { get; }
public ICommand SetLocalModeCommand { get; }
#endregion #endregion
@@ -174,32 +174,30 @@ namespace DeviceEditModule.ViewModels
SetOcpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OCP(OcpValue, Ct()); AppendLog($"OCP已设为 {OcpValue} A"); })); SetOcpCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OCP(OcpValue, Ct()); AppendLog($"OCP已设为 {OcpValue} A"); }));
SetOppCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OPP(OppValue, Ct()); AppendLog($"OPP已设为 {OppValue} W"); })); SetOppCommand = new DelegateCommand(async () => await Exec(async () => { await _device!._OPP(OppValue, Ct()); AppendLog($"OPP已设为 {OppValue} W"); }));
ClearAlarmCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("保护告警已清除"); })); ClearAlarmCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("保护告警已清除"); }));
SetRemoteModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到远程控制模式"); }));
SetLocalModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("已切换到本地控制模式"); }));
SetLoadValueCommand = new DelegateCommand(async () => await Exec(async () => SetLoadValueCommand = new DelegateCommand(async () => await Exec(async () =>
{ {
var mode = SelectedMode.ToUpperInvariant(); switch (SelectedMode)
switch (mode)
{ {
case "CC": case DeviceWorkMode.CC:
await _device!.CC(LoadValue, Ct()); await _device!.CC(LoadValue, Ct());
AppendLog($"恒流 CC 已设为 {LoadValue} A"); AppendLog($"恒流 CC 已设为 {LoadValue} A");
break; break;
case "CV": case DeviceWorkMode.CV:
await _device!.CV(LoadValue, Ct()); await _device!.CV(LoadValue, Ct());
AppendLog($"恒压 CV 已设为 {LoadValue} V"); AppendLog($"恒压 CV 已设为 {LoadValue} V");
break; break;
case "CP": case DeviceWorkMode.CP:
await _device!.CP(LoadValue, Ct()); await _device!.CP(LoadValue, Ct());
AppendLog($"恒功率 CP 已设为 {LoadValue} W"); AppendLog($"恒功率 CP 已设为 {LoadValue} W");
break; break;
case "CR": case DeviceWorkMode.CR:
await _device!.CR(LoadValue, Ct()); await _device!.CR(LoadValue, Ct());
AppendLog($"恒电阻 CR 已设为 {LoadValue} Ω"); AppendLog($"恒电阻 CR 已设为 {LoadValue} Ω");
break; break;
default: default:
AppendLog($"未知模式 {SelectedMode},无法设置设定值"); AppendLog($"模式 {SelectedMode} 不支持设置设定值");
break; break;
} }
})); }));

View File

@@ -1,11 +1,15 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
using static DeviceCommand.Devices.SDS2000X_HD;
namespace DeviceEditModule.ViewModels namespace DeviceEditModule.ViewModels
{ {
@@ -153,10 +157,14 @@ namespace DeviceEditModule.ViewModels
public ICommand SetVoltsDivCommand { get; } public ICommand SetVoltsDivCommand { get; }
public ICommand SetOffsetCommand { get; } public ICommand SetOffsetCommand { get; }
// 🛠️ 补全:设置阻抗的 Command
public ICommand SetImpedance50Command { get; }
public ICommand SetImpedance1MCommand { get; }
public ICommand SetTimeBaseCommand { get; } public ICommand SetTimeBaseCommand { get; }
public ICommand SetTriggerLevelCommand { get; } public ICommand SetTriggerLevelCommand { get; }
public ICommand SetTriggerSourceCommand { get; } public ICommand SetTriggerSourceCommand { get; }
public ICommand QueryMeasurementsCommand{ get; } public ICommand QueryMeasurementsCommand { get; }
public ICommand QueryVppCommand { get; } public ICommand QueryVppCommand { get; }
public ICommand QueryFrequencyCommand { get; } public ICommand QueryFrequencyCommand { get; }
public ICommand QueryRmsCommand { get; } public ICommand QueryRmsCommand { get; }
@@ -178,21 +186,62 @@ namespace DeviceEditModule.ViewModels
SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 电压档位已设为 {VoltsPerDiv} V/div"); })); SetVoltsDivCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, VoltsPerDiv, Ct()); AppendLog($"C{Channel} 电压档位已设为 {VoltsPerDiv} V/div"); }));
SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); })); SetOffsetCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, Offset, Ct()); AppendLog($"C{Channel} 垂直偏移已设为 {Offset} V"); }));
// 🛠️ 绑定:设置 50Ω 和 1MΩ 阻抗控制
SetImpedance50Command = new DelegateCommand(async () => await Exec(async () =>
{
await _device!.(Channel, ImpedanceType.FIFty, Ct());
Is50Ohm = true;
AppendLog($"C{Channel} 输入阻抗已设为 50Ω");
}));
SetImpedance1MCommand = new DelegateCommand(async () => await Exec(async () =>
{
await _device!.(Channel, ImpedanceType.ONEM, Ct());
Is50Ohm = false;
AppendLog($"C{Channel} 输入阻抗已设为 1MΩ");
}));
SetTimeBaseCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TimeBase, Ct()); AppendLog($"水平时基已设为 {TimeBase} s/div"); })); SetTimeBaseCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TimeBase, Ct()); AppendLog($"水平时基已设为 {TimeBase} s/div"); }));
SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TriggerLevel, Ct()); AppendLog($"触发电平已设为 {TriggerLevel} V"); })); SetTriggerLevelCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TriggerLevel, Ct()); AppendLog($"触发电平已设为 {TriggerLevel} V"); }));
SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TriggerSource, Ct()); AppendLog($"触发源已设为 {TriggerSource}"); })); SetTriggerSourceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(TriggerSource, Ct()); AppendLog($"触发源已设为 {TriggerSource}"); }));
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () => QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
{ {
MeasuredVpp = await _device!.(Channel, Ct()); // 1. 保留设备原生吐出的完整原始字符串
MeasuredFrequency = await _device!.(Channel, Ct()); string rawVpp = await _device!.(Channel, Ct());
MeasuredRms = await _device!.(Channel, Ct()); string rawFreq = await _device!.(Channel, Ct());
AppendLog($"C{Channel} 测量 → Vpp:{MeasuredVpp} Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V"); string rawRms = await _device!.(Channel, Ct());
// 2. 清洗数据并将科学计数法转为常规小数后赋值给 UI 属性
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 () => { MeasuredVpp = await _device!.(Channel, Ct()); AppendLog($"C{Channel} Vpp: {MeasuredVpp}"); })); QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredFrequency = await _device!.(Channel, Ct()); AppendLog($"C{Channel} Freq: {MeasuredFrequency}"); })); {
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () => { MeasuredRms = await _device!.(Channel, Ct()); AppendLog($"C{Channel} RMS: {MeasuredRms}"); })); string raw = await _device!.(Channel, Ct());
MeasuredVpp = ParseMeasurement(raw);
AppendLog($"C{Channel} Vpp: {MeasuredVpp} (Raw: {raw.Trim()})");
}));
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
{
string raw = await _device!.(Channel, Ct());
MeasuredFrequency = ParseMeasurement(raw);
AppendLog($"C{Channel} Freq: {MeasuredFrequency} (Raw: {raw.Trim()})");
}));
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
{
string raw = await _device!.(Channel, Ct());
MeasuredRms = ParseMeasurement(raw);
AppendLog($"C{Channel} RMS: {MeasuredRms} (Raw: {raw.Trim()})");
}));
Initialize(); Initialize();
} }
@@ -281,6 +330,52 @@ 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()

View File

@@ -1,8 +1,10 @@
using DeviceCommand.Device; using DeviceCommand.Devices;
using Prism.Commands; using Prism.Commands;
using Prism.Ioc; using Prism.Ioc;
using System; using System;
using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
@@ -57,29 +59,27 @@ namespace DeviceEditModule.ViewModels
set => SetProperty(ref _channel, value); set => SetProperty(ref _channel, value);
} }
private double _voltageRange = 300.0; private SPAW7000.VoltageRange _voltageRange = SPAW7000.VoltageRange.V_300;
/// <summary>电压量程V。</summary> /// <summary>电压量程枚举。</summary>
public double VoltageRange public SPAW7000.VoltageRange VoltageRange
{ {
get => _voltageRange; get => _voltageRange;
set => SetProperty(ref _voltageRange, value); set => SetProperty(ref _voltageRange, value);
} }
private double _currentRange = 5.0; private SPAW7000.CurrentRange _currentRange = SPAW7000.CurrentRange.A_5;
/// <summary>电流量程A。</summary> /// <summary>电流量程枚举。</summary>
public double CurrentRange public SPAW7000.CurrentRange CurrentRange
{ {
get => _currentRange; get => _currentRange;
set => SetProperty(ref _currentRange, value); set => SetProperty(ref _currentRange, value);
} }
private string _couplingMode = "DC"; public SPAW7000.VoltageRange[] VoltageRangeOptions { get; } =
/// <summary>耦合模式AC / DC / ACDC。</summary> (SPAW7000.VoltageRange[])Enum.GetValues(typeof(SPAW7000.VoltageRange));
public string CouplingMode
{ public SPAW7000.CurrentRange[] CurrentRangeOptions { get; } =
get => _couplingMode; (SPAW7000.CurrentRange[])Enum.GetValues(typeof(SPAW7000.CurrentRange));
set => SetProperty(ref _couplingMode, value);
}
private int _resolution = 6; private int _resolution = 6;
/// <summary>显示分辨率5 或 6。</summary> /// <summary>显示分辨率5 或 6。</summary>
@@ -167,7 +167,6 @@ namespace DeviceEditModule.ViewModels
public ICommand QueryAllMeasureCommand { get; } public ICommand QueryAllMeasureCommand { get; }
public ICommand SetVoltageRangeCommand { get; } public ICommand SetVoltageRangeCommand { get; }
public ICommand SetCurrentRangeCommand { get; } public ICommand SetCurrentRangeCommand { get; }
public ICommand SetCouplingModeCommand { get; }
public ICommand SetResolutionCommand { get; } public ICommand SetResolutionCommand { get; }
public ICommand SetBrightnessCommand { get; } public ICommand SetBrightnessCommand { get; }
public ICommand SetTouchLockOnCommand { get; } public ICommand SetTouchLockOnCommand { get; }
@@ -184,9 +183,8 @@ namespace DeviceEditModule.ViewModels
QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct())))); QueryIdentityCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("IDN: " + await _device!.(Ct()))));
ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("设备已重置"); })); ResetDeviceCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Ct()); AppendLog("设备已重置"); }));
SetVoltageRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, VoltageRange, Ct()); AppendLog($"通道 {Channel} 电压量程已设为 {VoltageRange} V"); })); SetVoltageRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, VoltageRange, Ct()); AppendLog($"通道 {Channel} 电压量程已设为 {(int)VoltageRange} V"); }));
SetCurrentRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, CurrentRange, Ct()); AppendLog($"通道 {Channel} 电流量程已设为 {CurrentRange} A"); })); SetCurrentRangeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, CurrentRange, Ct()); AppendLog($"通道 {Channel} 电流量程已设为 {(int)CurrentRange} A"); }));
SetCouplingModeCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Channel, CouplingMode, Ct()); AppendLog($"通道 {Channel} 耦合模式已设为 {CouplingMode}"); }));
SetResolutionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Resolution, Ct()); AppendLog($"显示分辨率已设为 {Resolution} 位"); })); SetResolutionCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Resolution, Ct()); AppendLog($"显示分辨率已设为 {Resolution} 位"); }));
SetBrightnessCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Brightness, Ct()); AppendLog($"屏幕亮度已设为 {Brightness}"); })); SetBrightnessCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(Brightness, Ct()); AppendLog($"屏幕亮度已设为 {Brightness}"); }));
SetTouchLockOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(true, Ct()); AppendLog("屏幕触摸已锁定"); })); SetTouchLockOnCommand = new DelegateCommand(async () => await Exec(async () => { await _device!.(true, Ct()); AppendLog("屏幕触摸已锁定"); }));
@@ -195,13 +193,22 @@ namespace DeviceEditModule.ViewModels
QuerySerialCommand = new DelegateCommand(async () => await Exec(async () => { DeviceSerial = await _device!.(Ct()); AppendLog($"序列号: {DeviceSerial}"); })); QuerySerialCommand = new DelegateCommand(async () => await Exec(async () => { DeviceSerial = await _device!.(Ct()); AppendLog($"序列号: {DeviceSerial}"); }));
QueryStatusByteCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("STB: " + await _device!.(Ct())))); QueryStatusByteCommand = new DelegateCommand(async () => await Exec(async () => AppendLog("STB: " + await _device!.(Ct()))));
// 只修改这里:保持你原本的 5 次驱动查询调用不变,仅对读回来的科学计数法进行两位小数格式化
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () => QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
{ {
MeasuredVoltage = await _device!.(Channel, Ct()); string rawU = await _device!.(Channel, Ct());
MeasuredCurrent = await _device!.(Channel, Ct()); string rawI = await _device!.(Channel, Ct());
MeasuredPower = await _device!.(Channel, Ct()); string rawP = await _device!.(Channel, Ct());
MeasuredFrequency = await _device!.(Channel, Ct()); string rawF = await _device!.(Channel, Ct());
MeasuredPowerFactor = await _device!.(Channel, Ct()); string rawPF = await _device!.(Channel, Ct());
// 转换科学计数法格式,如果失败会自动保持原样
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}"); AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage}V I:{MeasuredCurrent}A P:{MeasuredPower}W F:{MeasuredFrequency}Hz PF:{MeasuredPowerFactor}");
})); }));
@@ -256,6 +263,41 @@ 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)

View File

@@ -9,7 +9,7 @@
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
Height="850" Width="900"> Height="900" Width="900">
<!-- 此窗口独立开启 ShowInTaskbar以支持最小化后在任务栏恢复 --> <!-- 此窗口独立开启 ShowInTaskbar以支持最小化后在任务栏恢复 -->
<prism:Dialog.WindowStyle> <prism:Dialog.WindowStyle>
@@ -124,7 +124,7 @@
<!-- 标题栏 --> <!-- 标题栏 -->
<RowDefinition Height="38"/> <RowDefinition Height="38"/>
<!-- Tab 标签条 --> <!-- Tab 标签条 -->
<RowDefinition Height="40"/> <RowDefinition Height="80"/>
<!-- 内容区 --> <!-- 内容区 -->
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>

View File

@@ -7,7 +7,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>

View File

@@ -7,7 +7,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>

View File

@@ -7,7 +7,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>

View File

@@ -7,7 +7,7 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>
@@ -100,14 +100,15 @@
<!-- 工作模式 --> <!-- 工作模式 -->
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="工作模式" Style="{StaticResource ParamLabel}"/> <TextBlock Text="工作模式" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="90" Height="32" Margin="4,0" <ComboBox xmlns:sys="clr-namespace:System;assembly=mscorlib"
Width="90" Height="32" Margin="4,0"
materialDesign:HintAssist.Hint="" materialDesign:HintAssist.Hint=""
SelectedItem="{Binding SelectedMode}" SelectedItem="{Binding SelectedMode}"
VerticalContentAlignment="Center" FontSize="12"> VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="CC"/> <sys:String>CC</sys:String>
<ComboBoxItem Content="CV"/> <sys:String>CV</sys:String>
<ComboBoxItem Content="CP"/> <sys:String>CP</sys:String>
<ComboBoxItem Content="CR"/> <sys:String>CR</sys:String>
</ComboBox> </ComboBox>
<Button Content="设置" Command="{Binding SetModeCommand}" <Button Content="设置" Command="{Binding SetModeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
@@ -115,13 +116,13 @@
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>
<!-- 远程/本地 --> <!-- 远程/本地 -->
<StackPanel Orientation="Horizontal" Margin="0,4"> <!--<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="控制模式" Style="{StaticResource ParamLabel}"/> <TextBlock Text="控制模式" Style="{StaticResource ParamLabel}"/>
<Button Content="远程控制" Command="{Binding SetRemoteModeCommand}" <Button Content="远程控制" Command="{Binding SetRemoteModeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
<Button Content="本地控制" Command="{Binding SetLocalModeCommand}" <Button Content="本地控制" Command="{Binding SetLocalModeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>-->
<!-- 系统操作 --> <!-- 系统操作 -->
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="系统操作" Style="{StaticResource ParamLabel}"/> <TextBlock Text="系统操作" Style="{StaticResource ParamLabel}"/>

View File

@@ -4,10 +4,11 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>
@@ -92,10 +93,10 @@
<ComboBox Width="80" Height="32" Margin="4,0" <ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Channel}" SelectedItem="{Binding Channel}"
VerticalContentAlignment="Center" FontSize="12"> VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="1"/> <sys:Int32>1</sys:Int32>
<ComboBoxItem Content="2"/> <sys:Int32>2</sys:Int32>
<ComboBoxItem Content="3"/> <sys:Int32>3</sys:Int32>
<ComboBoxItem Content="4"/> <sys:Int32>4</sys:Int32>
</ComboBox> </ComboBox>
<Button Content="开启" Command="{Binding SetChannelOnCommand}" <Button Content="开启" Command="{Binding SetChannelOnCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
@@ -137,7 +138,7 @@
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/> <TextBlock Text="V/div (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" <TextBox Style="{StaticResource NumInput}"
Text="{Binding VoltsPerDiv, UpdateSourceTrigger=PropertyChanged}"/> Text="{Binding VoltsPerDiv}"/>
<Button Content="设置" Command="{Binding SetVoltsDivCommand}" <Button Content="设置" Command="{Binding SetVoltsDivCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>
@@ -165,7 +166,7 @@
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="时基 (s/div)" Style="{StaticResource ParamLabel}"/> <TextBlock Text="时基 (s/div)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" <TextBox Style="{StaticResource NumInput}"
Text="{Binding TimeBase, UpdateSourceTrigger=PropertyChanged}"/> Text="{Binding TimeBase}"/>
<Button Content="设置" Command="{Binding SetTimeBaseCommand}" <Button Content="设置" Command="{Binding SetTimeBaseCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>

View File

@@ -5,9 +5,10 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare" xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d" mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="False"
d:DesignHeight="760" d:DesignWidth="860"> d:DesignHeight="760" d:DesignWidth="860">
<UserControl.Resources> <UserControl.Resources>
@@ -92,38 +93,44 @@
<ComboBox Width="80" Height="32" Margin="4,0" <ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Channel}" SelectedItem="{Binding Channel}"
VerticalContentAlignment="Center" FontSize="12"> VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="1"/> <sys:Int32>1</sys:Int32>
<ComboBoxItem Content="2"/> <sys:Int32>2</sys:Int32>
<ComboBoxItem Content="3"/> <sys:Int32>3</sys:Int32>
<ComboBoxItem Content="4"/> <sys:Int32>4</sys:Int32>
<sys:Int32>5</sys:Int32>
<sys:Int32>6</sys:Int32>
<sys:Int32>7</sys:Int32>
<sys:Int32>8</sys:Int32>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电压量程 (V)" Style="{StaticResource ParamLabel}"/> <TextBlock Text="电压量程 (V)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" <ComboBox Width="100" Height="32" Margin="4,0"
Text="{Binding VoltageRange, UpdateSourceTrigger=PropertyChanged}"/> ItemsSource="{Binding VoltageRangeOptions}"
SelectedItem="{Binding VoltageRange}"
VerticalContentAlignment="Center" FontSize="12"/>
<Button Content="设置" Command="{Binding SetVoltageRangeCommand}" <Button Content="设置" Command="{Binding SetVoltageRangeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电流量程 (A)" Style="{StaticResource ParamLabel}"/> <TextBlock Text="电流量程 (A)" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource NumInput}" <ComboBox Width="100" Height="32" Margin="4,0"
Text="{Binding CurrentRange, UpdateSourceTrigger=PropertyChanged}"/> ItemsSource="{Binding CurrentRangeOptions}"
SelectedItem="{Binding CurrentRange}"
VerticalContentAlignment="Center" FontSize="12"/>
<Button Content="设置" Command="{Binding SetCurrentRangeCommand}" <Button Content="设置" Command="{Binding SetCurrentRangeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4"> <!--<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}"/> <TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}"/>
<ComboBox Width="90" Height="32" Margin="4,0" <ComboBox Width="90" Height="32" Margin="4,0"
SelectedItem="{Binding CouplingMode}" SelectedItem="{Binding CouplingMode}"
ItemsSource="{Binding InputMeasureModeOptions}"
VerticalContentAlignment="Center" FontSize="12"> VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="AC"/>
<ComboBoxItem Content="DC"/>
<ComboBoxItem Content="ACDC"/>
</ComboBox> </ComboBox>
<Button Content="设置" Command="{Binding SetCouplingModeCommand}" <Button Content="设置" Command="{Binding SetCouplingModeCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
</StackPanel> </StackPanel>-->
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
@@ -136,8 +143,8 @@
<ComboBox Width="80" Height="32" Margin="4,0" <ComboBox Width="80" Height="32" Margin="4,0"
SelectedItem="{Binding Resolution}" SelectedItem="{Binding Resolution}"
VerticalContentAlignment="Center" FontSize="12"> VerticalContentAlignment="Center" FontSize="12">
<ComboBoxItem Content="5"/> <sys:Int32>5</sys:Int32>
<ComboBoxItem Content="6"/> <sys:Int32>6</sys:Int32>
</ComboBox> </ComboBox>
<Button Content="设置" Command="{Binding SetResolutionCommand}" <Button Content="设置" Command="{Binding SetResolutionCommand}"
Style="{StaticResource CmdBtn}"/> Style="{StaticResource CmdBtn}"/>
@@ -179,12 +186,12 @@
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="型号" Style="{StaticResource ParamLabel}"/> <TextBlock Text="型号" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="120" <TextBox Style="{StaticResource MeasureBox}" Width="281"
Text="{Binding DeviceModel, Mode=OneWay}"/> Text="{Binding DeviceModel, Mode=OneWay}"/>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4"> <StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="序列号" Style="{StaticResource ParamLabel}"/> <TextBlock Text="序列号" Style="{StaticResource ParamLabel}"/>
<TextBox Style="{StaticResource MeasureBox}" Width="160" <TextBox Style="{StaticResource MeasureBox}" Width="281"
Text="{Binding DeviceSerial, Mode=OneWay}"/> Text="{Binding DeviceSerial, Mode=OneWay}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -0,0 +1,20 @@
using ExportModule.Views;
using System.Reflection;
namespace ExportModule
{
public class ExportModule: IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<ExportView>("ExportView");
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.104.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ORM\ORM.csproj" />
<ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,262 @@
using ClosedXML.Excel;
using Logger;
using Microsoft.Win32;
using Model;
using Model.Entity;
using Model.Models;
using Service.Interface;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace ExportModule.ViewModels
{
public class ExportViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
private ObservableCollection<TestReportModel> _testReportModelList = new();
public ObservableCollection<TestReportModel> TestReportModelList
{
get { return _testReportModelList; }
set { SetProperty(ref _testReportModelList, value); }
}
private DateTime? _startTime;
public DateTime? StartTime
{
get => _startTime;
set => SetProperty(ref _startTime, value);
}
private DateTime? _endTime;
public DateTime? EndTime
{
get => _endTime;
set => SetProperty(ref _endTime, value);
}
private string _selectedScope = "全部";
public string SelectedScope
{
get => _selectedScope;
set => SetProperty(ref _selectedScope, value);
}
public ObservableCollection<string> ScopeOptions { get; } = new()
{
"全部",
"TestCell1", "TestCell2", "TestCell3", "TestCell4",
"TestCell5", "TestCell6", "TestCell7", "TestCell8"
};
private string _statusMessage = "就绪";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
private int _totalCount;
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
private TestReportModel? _selectedTestReport;
public TestReportModel? SelectedTestReport
{
get => _selectedTestReport;
set => SetProperty(ref _selectedTestReport, value);
}
#endregion
#region
public ICommand LoadedCommand { get; }
public ICommand QueryCommand { get; }
public ICommand ExportCommand { get; }
#endregion
#region
private readonly ITestReportService _testReportService;
#endregion
public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_testReportService = containerProvider.Resolve<ITestReportService>();
LoadedCommand = new AsyncDelegateCommand(OnLoad);
QueryCommand = new AsyncDelegateCommand(OnQuery);
ExportCommand = new AsyncDelegateCommand(OnExport);
}
#region
/// <summary>
/// 加载全部(无筛选条件)
/// </summary>
private async Task OnLoad()
{
var result = await _testReportService.GetFilterList();
if (result.IsSuccess)
{
TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"加载完成,共 {TotalCount} 条记录", () => { });
}
else
{
TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"加载失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
}
}
/// <summary>
/// 查询(根据开始日期、结束日期、台架名称筛选)
/// </summary>
private async Task OnQuery()
{
// 处理结束日期:若用户只选了日期没选时间,默认到当天末尾
DateTime? queryEnd = EndTime;
if (queryEnd.HasValue)
{
queryEnd = queryEnd.Value.Date.AddDays(1).AddSeconds(-1);
}
string? scope = SelectedScope == "全部" ? null : SelectedScope;
var result = await _testReportService.GetFilterList(StartTime, queryEnd, scope);
if (result.IsSuccess)
{
TestReportModelList = new ObservableCollection<TestReportModel>(result.Data ?? new List<TestReportModel>());
TotalCount = TestReportModelList.Count;
ShowInfoMessageBox($"查询完成,共 {TotalCount} 条记录", () => { });
}
else
{
TestReportModelList = new ObservableCollection<TestReportModel>();
TotalCount = 0;
ShowErrorMessageBox($"查询失败:{result.Msg}", () => { });
LoggerHelper.Error(result.Msg);
}
}
/// <summary>
/// 导出 Excel根据用户在表格中选中的 TestReportModel.TestRoundId 查询并导出完整步骤数据
/// </summary>
private async Task OnExport()
{
if (SelectedTestReport == null)
{
ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { });
return;
}
StatusMessage = "正在查询导出数据...";
var entityResult = await _testReportService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId);
if (!entityResult.IsSuccess || entityResult.Data == null || entityResult.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 entities = entityResult.Data;
StatusMessage = $"正在导出 {entities.Count} 条步骤记录...";
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
}
#endregion
#region
private static void ExportToExcel(string filePath, List<TestReportEntity> entities)
{
using var workbook = new XLWorkbook();
var ws = workbook.Worksheets.Add("测试报告");
// 表头
string[] headers = {
"Id", "CreateTime", "TestRoundId", "Scope", "FileName",
"StepIndex", "StepName", "StepType", "MethodName", "MethodFullName",
"Result", "RunTimeMs", "OutputValue", "Depth", "IsErrorStep", "LoopRemaining"
};
for (int c = 0; c < headers.Length; c++)
{
ws.Cell(1, c + 1).Value = headers[c];
}
// 标题行样式
var headerRange = ws.Range(1, 1, 1, headers.Length);
headerRange.Style.Font.Bold = true;
headerRange.Style.Fill.BackgroundColor = XLColor.FromArgb(0xECEFF4);
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
// 填充数据
for (int i = 0; i < entities.Count; i++)
{
var e = entities[i];
int row = i + 2;
ws.Cell(row, 1).Value = e.Id;
ws.Cell(row, 2).Value = e.CreateTime.ToString("yyyy-MM-dd HH:mm:ss");
ws.Cell(row, 3).Value = e.TestRoundId.ToString();
ws.Cell(row, 4).Value = e.Scope ?? "";
ws.Cell(row, 5).Value = e.FileName ?? "";
ws.Cell(row, 6).Value = e.StepIndex;
ws.Cell(row, 7).Value = e.StepName ?? "";
ws.Cell(row, 8).Value = e.StepType ?? "";
ws.Cell(row, 9).Value = e.MethodName ?? "";
ws.Cell(row, 10).Value = e.MethodFullName ?? "";
ws.Cell(row, 11).Value = e.Result;
ws.Cell(row, 12).Value = e.RunTimeMs;
ws.Cell(row, 13).Value = e.OutputValue ?? "";
ws.Cell(row, 14).Value = e.Depth;
ws.Cell(row, 15).Value = e.IsErrorStep ? "是" : "否";
ws.Cell(row, 16).Value = e.LoopRemaining;
// 数据行边框
var dataRange = ws.Range(row, 1, row, headers.Length);
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
}
ws.Columns().AdjustToContents();
workbook.SaveAs(filePath);
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
}
public void Dispose()
{
}
#endregion
}
}

View File

@@ -0,0 +1,204 @@
<UserControl x:Class="ExportModule.Views.ExportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="700"
d:DesignWidth="1200">
<UserControl.Resources>
<converters:LessThanConverter x:Key="LessThanConverter"/>
</UserControl.Resources>
<Border Background="#F5F7FA">
<Grid x:Name="RootGrid" Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ========== 标题 ========== -->
<TextBlock Grid.Row="0"
Text="测试报告导出"
FontSize="20" FontWeight="Bold"
Margin="4,4,0,12"/>
<!-- ========== 主内容卡片 ========== -->
<Border Grid.Row="1"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<!-- 工具栏 -->
<RowDefinition Height="Auto"/>
<!-- 数据表格 -->
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- ====== 工具栏(筛选条件 + 操作按钮) ====== -->
<Border Grid.Row="0"
Background="#ECEFF4"
Padding="12,8"
BorderBrush="#DDD" BorderThickness="0,0,0,1">
<!-- 第一行:筛选条件 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 开始日期 -->
<TextBlock Grid.Column="0"
Text="开始日期:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<DatePicker Grid.Column="1"
materialDesign:HintAssist.Hint=""
SelectedDate="{Binding StartTime}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 结束日期 -->
<TextBlock Grid.Column="2"
Text="结束日期:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<DatePicker Grid.Column="3"
materialDesign:HintAssist.Hint=""
SelectedDate="{Binding EndTime}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 台架选择 -->
<TextBlock Grid.Column="4"
Text="台架:"
VerticalAlignment="Center"
FontWeight="SemiBold"
Margin="0,0,4,0"/>
<ComboBox Grid.Column="5"
materialDesign:HintAssist.Hint=""
ItemsSource="{Binding ScopeOptions}"
SelectedItem="{Binding SelectedScope}"
VerticalAlignment="Center"
Margin="0,0,16,0"/>
<!-- 操作按钮 -->
<Button Grid.Column="6"
Content="查询"
Command="{Binding QueryCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
<Button Grid.Column="7"
Content="导出 Excel"
Command="{Binding ExportCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
<Button Grid.Column="8"
Content="加载全部"
Command="{Binding LoadedCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Padding="16,6"
Margin="4,0"
VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- ====== 数据展示区 ====== -->
<DataGrid Grid.Row="1"
ItemsSource="{Binding TestReportModelList}"
AutoGenerateColumns="False"
IsReadOnly="True"
RowHeaderWidth="0"
BorderThickness="0"
Background="White"
AlternatingRowBackground="#F5F7FA"
SelectionMode="Single"
SelectedItem="{Binding SelectedTestReport}"
Margin="8">
<DataGrid.Columns>
<DataGridTextColumn Header="TestRoundId"
Width="280"
Binding="{Binding TestRoundId}"/>
<DataGridTextColumn Header="台架名称"
Width="120"
Binding="{Binding Scope}"/>
<DataGridTextColumn Header="开始时间"
Width="180"
Binding="{Binding StartTime, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"/>
<DataGridTextColumn Header="结束时间"
Width="180"
Binding="{Binding EndTime, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}"/>
<DataGridTextColumn Header="文件名"
Width="*"
Binding="{Binding FileName}"/>
</DataGrid.Columns>
<DataGrid.Resources>
<!-- 标题行样式:与项目风格一致 -->
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#ECEFF4"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="BorderBrush" Value="#DDD"/>
<Setter Property="BorderThickness" Value="0,0,1,1"/>
<Setter Property="Padding" Value="8,6"/>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
</DataGrid>
</Grid>
</Border>
<!-- ========== 状态栏 ========== -->
<Border Grid.Row="2"
Background="#ECEFF4"
Padding="10,6"
Margin="0,8,0,0"
CornerRadius="2">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding StatusMessage}"
Foreground="#444"
FontSize="12"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
Foreground="#666"
FontSize="12"
VerticalAlignment="Center"
Margin="8,0,0,0">
<Run Text="记录数:"/>
<Run Text="{Binding TotalCount}"/>
</TextBlock>
</Grid>
</Border>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace ExportModule.Views
{
/// <summary>
/// ExportView.xaml 的交互逻辑
/// </summary>
public partial class ExportView : UserControl
{
public ExportView()
{
InitializeComponent();
}
}
}

View File

@@ -1,5 +1,5 @@
using DeviceCommand.Base; using DeviceCommand.Base;
using DeviceCommand.Device; using DeviceCommand.Devices;
using MainModule.Views; using MainModule.Views;
using System.Reflection; using System.Reflection;
using UIShare.GlobalVariable; using UIShare.GlobalVariable;

View File

@@ -1,6 +1,9 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Input; using System.Windows.Input;
using DeviceCommand.Devices;
using Logger; using Logger;
using Prism.Ioc; using Prism.Ioc;
using TestingModule.ViewModels; using TestingModule.ViewModels;
@@ -19,6 +22,8 @@ namespace MainModule.ViewModels
private string _testStatus; private string _testStatus;
private readonly IScopedProvider _scope; private readonly IScopedProvider _scope;
private bool IsInitialized =false; private bool IsInitialized =false;
private bool _isRunningErrorSteps = false;
private SubscriptionToken? _alarmToken;
#endregion #endregion
#region #region
@@ -64,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>();
@@ -78,8 +87,110 @@ namespace MainModule.ViewModels
RefreshCommand = new DelegateCommand(OnRefresh); RefreshCommand = new DelegateCommand(OnRefresh);
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol); BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
LoadedCommand = new AsyncDelegateCommand(OnLoad); LoadedCommand = new AsyncDelegateCommand(OnLoad);
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope));
_alarmToken = _eventAggregator.GetEvent<AlarmEvent>().Subscribe(OnAlarmTriggered);
} }
private async Task SilenceBuzzer(string scope)
{
if (_deviceManager.IOGroup == null) return;
if (scope == "default")
{
for(int i = 1; i <= 8; i++)
{
await _deviceManager.IOGroup.Async(i, ., false);
}
}
else if (scope == TestStatus)
{
int = _systemConfig.SharedParameterList
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
if ( >= 1 && <= 8)
{
await _deviceManager.IOGroup.Async(, ., false);
}
}
}
/// <summary>
/// AlarmEvent 回调:
/// 上下限 → 蜂鸣器报警 true
/// 上下极限 → 蜂鸣器报警 true + 执行对应 scope 的异常流程
/// </summary>
private async void OnAlarmTriggered((string Scope, string Fingerprint, string Status) args)
{
if (args.Scope != TestStatus) return;
if (_deviceManager?.IOGroup == null) return;
int = _systemConfig.SharedParameterList
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
if ( < 1 || > 8) return;
// 上下限 / 上下极限 均触发蜂鸣器报警
await _deviceManager.IOGroup.Async(, ., true);
LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。");
// 上下极限额外触发异常流程
if (args.Status == "超上极限" || args.Status == "超下极限")
{
_ = TriggerErrorStepsAsync();
}
}
/// <summary>
/// 执行异常流程(带并发保护,同一时间仅允许一次)。
/// 如果正常流程正在运行,先取消并等待其停止,再执行异常流程。
/// </summary>
private async Task TriggerErrorStepsAsync()
{
if (_isRunningErrorSteps) return;
_isRunningErrorSteps = true;
try
{
// 1. 如果正常流程正在运行,先停止它
// IsStop == false 表示正在运行IsStop == null 表示未启动或已结束
if (_scopedContext.IsStop == false)
{
LoggerHelper.WarnWithNotify(TestStatus, $"[{TestStatus}] 报警触发,正在停止正常流程...");
_stepRunning.stepCTS.Cancel();
// 等待 ShellViewModel 清理完毕IsStop 变回 null、stepCTS 被重置)
// ExecuteMethodStep 内部硬编码使用 stepCTS.Token必须等它重置后才能跑异常流程
var deadline = DateTime.Now.AddSeconds(5);
while ((_scopedContext.IsStop != null || _stepRunning.stepCTS.IsCancellationRequested)
&& DateTime.Now < deadline)
{
await Task.Delay(50);
}
// 超时后仍未重置则强制重置,确保异常流程的设备命令能拿到有效 Token
if (_stepRunning.stepCTS.IsCancellationRequested)
{
_stepRunning.stepCTS = new CancellationTokenSource();
_scopedContext.IsStop = null;
_scopedContext.RunState = "运行";
}
LoggerHelper.WarnWithNotify(TestStatus, $"[{TestStatus}] 正常流程已停止,开始执行异常流程");
}
// 2. 执行异常流程
await _stepRunning.ExecuteErrorSteps(
_scopedContext.Program,
cancellationToken: _stepRunning.errorStepCTS.Token);
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(TestStatus, $"[{TestStatus}] 报警触发异常流程失败: {ex.Message}");
}
finally
{
_isRunningErrorSteps = false;
if (_stepRunning.errorStepCTS.IsCancellationRequested)
_stepRunning.errorStepCTS = new CancellationTokenSource();
}
}
public void Dispose() public void Dispose()
{ {
@@ -92,6 +203,9 @@ namespace MainModule.ViewModels
try try
{ {
if (_alarmToken != null)
_eventAggregator.GetEvent<AlarmEvent>().Unsubscribe(_alarmToken);
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(async (scope) => await SilenceBuzzer(scope));
// 2. 显式释放硬件资源(防止端口占用/死锁) // 2. 显式释放硬件资源(防止端口占用/死锁)
if (_deviceManager is IDisposable disposableDevice) if (_deviceManager is IDisposable disposableDevice)
{ {
@@ -156,6 +270,7 @@ namespace MainModule.ViewModels
if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath)) if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath))
{ {
var filePath = _systemConfig.DefaultProgramFilePath; var filePath = _systemConfig.DefaultProgramFilePath;
_systemConfig.CurrentADPFile= _systemConfig.DefaultProgramFilePath;
// 读取 JSON 文件 // 读取 JSON 文件
string json = File.ReadAllText(filePath); string json = File.ReadAllText(filePath);

View File

@@ -231,7 +231,7 @@
</ContentControl> </ContentControl>
<!-- 单元 9TestCell9默认 (2,2) --> <!-- 单元 9TestCell9默认 (2,2) -->
<ContentControl prism:RegionManager.RegionName="TestCell9" Margin="2"> <!--<ContentControl prism:RegionManager.RegionName="TestCell9" Margin="2">
<ContentControl.Style> <ContentControl.Style>
<Style TargetType="ContentControl"> <Style TargetType="ContentControl">
<Setter Property="Grid.Row" Value="2"/> <Setter Property="Grid.Row" Value="2"/>
@@ -253,6 +253,6 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</ContentControl.Style> </ContentControl.Style>
</ContentControl> </ContentControl>-->
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -0,0 +1,67 @@
using SqlSugar;
using System;
namespace Model.Entity
{
/// <summary>
/// 测试报告记录:每次 ExecuteSteps / ExecuteErrorSteps 中每个步骤的执行结果。
/// 同一次运行的所有步骤共享同一个 TestRoundId。
/// </summary>
public class TestReportEntity : 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>步骤序号</summary>
[SugarColumn(ColumnName = "StepIndex")]
public int StepIndex { get; set; }
/// <summary>步骤名称</summary>
[SugarColumn(ColumnName = "StepName", Length = 200)]
public string StepName { get; set; } = string.Empty;
/// <summary>步骤类型(普通步骤/循环开始/循环结束)</summary>
[SugarColumn(ColumnName = "StepType", Length = 50)]
public string StepType { get; set; } = string.Empty;
/// <summary>执行的方法名称</summary>
[SugarColumn(ColumnName = "MethodName", Length = 200, IsNullable = true)]
public string? MethodName { get; set; }
/// <summary>方法的完整类型名(实际方法所属类)</summary>
[SugarColumn(ColumnName = "MethodFullName", Length = 500, IsNullable = true)]
public string? MethodFullName { get; set; }
/// <summary>执行结果(成功/失败/未执行/跳过)</summary>
[SugarColumn(ColumnName = "Result", Length = 20)]
public string Result { get; set; } = string.Empty;
/// <summary>执行耗时(毫秒)</summary>
[SugarColumn(ColumnName = "RunTimeMs", IsNullable = true)]
public int? RunTimeMs { get; set; }
/// <summary>输出值(如果有)</summary>
[SugarColumn(ColumnName = "OutputValue", Length = 500, IsNullable = true)]
public string? OutputValue { get; set; }
/// <summary>子程序嵌套深度0=主程序)</summary>
[SugarColumn(ColumnName = "Depth")]
public int Depth { get; set; }
/// <summary>是否异常流程步骤</summary>
[SugarColumn(ColumnName = "IsErrorStep")]
public bool IsErrorStep { get; set; }
/// <summary>循环剩余次数(仅循环步骤有值)</summary>
[SugarColumn(ColumnName = "LoopRemaining", IsNullable = true)]
public int? LoopRemaining { get; set; }
}
}

View File

@@ -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>

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model.Models
{
public class TestReportModel
{
public Guid TestRoundId { get; set; }
public string Scope { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -111,26 +111,36 @@ namespace MonitorModule.ViewModels.Dialogs
} }
// 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList // 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList
foreach (var signal in DiscoverDeviceSignals()) foreach (var (displayName, fingerprint, methodName) in DiscoverDeviceSignals())
{
//DeviceSingleList.Add(signal);
EnsureValueLimit(signal);
}
// 3. 发现 CAN 信号并加入 ValueLimitList / DeviceSingleList
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
{ {
//DeviceSingleList.Add(displayName); //DeviceSingleList.Add(displayName);
_canSignalMap[displayName] = (fingerprint, methodName); EnsureValueLimit(fingerprint, displayName, displayName, methodName);
EnsureValueLimit(displayName);
} }
// 3. 发现 CAN 信号并加入 ValueLimitList同时收集有效信号名
var validCanSignalNames = new HashSet<string>();
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
{
_canSignalMap[displayName] = (fingerprint, methodName);
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
validCanSignalNames.Add(displayName);
}
// 4. 清理已不存在于 DBC 中的 CAN 信号Fingerprint 以 "CAN" 开头且不在有效集合中)
var staleCanSignals = ValueLimitList
.Where(x => !string.IsNullOrEmpty(x.Fingerprint) &&
x.Fingerprint.StartsWith("CAN") &&
!validCanSignalNames.Contains(x.SignalName))
.ToList();
foreach (var item in staleCanSignals)
ValueLimitList.Remove(item);
} }
/// <summary> /// <summary>
/// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。 /// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。
/// 返回 DisplayName 列表。 /// 返回 (DisplayName, Fingerprint, MethodName) 列表。
/// </summary> /// </summary>
private IEnumerable<string> DiscoverDeviceSignals() private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverDeviceSignals()
{ {
if (_deviceManager?.DeviceMap == null) yield break; if (_deviceManager?.DeviceMap == null) yield break;
@@ -159,7 +169,7 @@ namespace MonitorModule.ViewModels.Dialogs
string displayName = !string.IsNullOrEmpty(attr?.Description) string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}" ? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}"; : $"{deviceName}.{method.Name}";
yield return displayName; yield return (displayName, fingerprint, method.Name);
} }
} }
} }
@@ -191,12 +201,13 @@ namespace MonitorModule.ViewModels.Dialogs
if (_systemConfig?.ConfigurationList == null) yield break; if (_systemConfig?.ConfigurationList == null) yield break;
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
foreach (var cfg in _systemConfig.ConfigurationList) foreach (var cfg in _systemConfig.ConfigurationList)
{ {
if (string.IsNullOrEmpty(cfg.SignalName)) continue; if (string.IsNullOrEmpty(cfg.SignalName)) continue;
if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue; if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue;
string fingerprint = CANSignalBroadcaster.BuildFingerprint((uint)cfg.Channel); string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, (uint)cfg.Channel);
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName); string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
@@ -224,13 +235,16 @@ namespace MonitorModule.ViewModels.Dialogs
/// <summary> /// <summary>
/// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。 /// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。
/// </summary> /// </summary>
private void EnsureValueLimit(string signalName) private void EnsureValueLimit(string fingerprint, string displayName, string signalName, string methodName = "")
{ {
if (ValueLimitList.Any(x => x.SignalName == signalName)) return; if (ValueLimitList.Any(x => x.SignalName == signalName)) return;
ValueLimitList.Add(new ValueLimitVM ValueLimitList.Add(new ValueLimitVM
{ {
DisplayName = displayName,
Fingerprint = fingerprint,
SignalName = signalName, SignalName = signalName,
MethodName = methodName,
Upper = 9999, Upper = 9999,
Lower = -9999, Lower = -9999,
UpperExtreme = 9999, UpperExtreme = 9999,
@@ -323,7 +337,8 @@ namespace MonitorModule.ViewModels.Dialogs
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
if (channel < 0 || channel >= msgDb.Count) return; if (channel < 0 || channel >= msgDb.Count) return;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel); string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
// 1. 移除该通道中已失效的 CAN 信号限制项 // 1. 移除该通道中已失效的 CAN 信号限制项
var staleSignals = _canSignalMap var staleSignals = _canSignalMap
@@ -353,7 +368,7 @@ namespace MonitorModule.ViewModels.Dialogs
_canSignalMap[displayName] = (fingerprint, methodName); _canSignalMap[displayName] = (fingerprint, methodName);
//if (!DeviceSingleList.Contains(displayName)) //if (!DeviceSingleList.Contains(displayName))
// DeviceSingleList.Add(displayName); // DeviceSingleList.Add(displayName);
EnsureValueLimit(displayName); EnsureValueLimit(fingerprint, displayName, displayName, methodName);
} }
} }
} }
@@ -366,7 +381,8 @@ namespace MonitorModule.ViewModels.Dialogs
{ {
if (args.Scope != _systemConfig?.Title) return; if (args.Scope != _systemConfig?.Title) return;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel); string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
var toRemove = _canSignalMap var toRemove = _canSignalMap
.Where(kvp => kvp.Value.Fingerprint == fingerprint) .Where(kvp => kvp.Value.Fingerprint == fingerprint)
.Select(kvp => kvp.Key) .Select(kvp => kvp.Key)

View File

@@ -222,10 +222,8 @@ namespace MonitorModule.ViewModels
if (channel == null) return; if (channel == null) return;
double time = _stopwatch.Elapsed.TotalSeconds;
// 记录数据点线程安全Record 内部 ConcurrentQueue + Series 操作) // 记录数据点线程安全Record 内部 ConcurrentQueue + Series 操作)
channel.Record(time, args.Value); channel.Record(args.Time, args.Value);
// 入队数据库批量写入 // 入队数据库批量写入
// CreateTime 使用 args.Time采样触发时刻而不是 DateTime.Now设备响应到达时刻 // CreateTime 使用 args.Time采样触发时刻而不是 DateTime.Now设备响应到达时刻
@@ -252,13 +250,12 @@ namespace MonitorModule.ViewModels
{ {
if (!_stopwatch.IsRunning) return; if (!_stopwatch.IsRunning) return;
double elapsed = _stopwatch.Elapsed.TotalSeconds; var now = DateTime.Now;
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom); var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null) if (xAxis != null)
{ {
double window = 10000 * 0.1; // 20s 视窗 xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20));
xAxis.Minimum = Math.Max(0, elapsed - window); xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5));
xAxis.Maximum = elapsed + 0.5;
} }
Plot.InvalidatePlot(true); Plot.InvalidatePlot(true);
} }
@@ -370,10 +367,11 @@ namespace MonitorModule.ViewModels
PlotAreaBorderColor = OxyColors.LightGray, PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White Background = OxyColors.White
}; };
pm.Axes.Add(new LinearAxis pm.Axes.Add(new DateTimeAxis
{ {
Position = AxisPosition.Bottom, Position = AxisPosition.Bottom,
Title = "时间 (s)", Title = "时间",
StringFormat = "HH:mm:ss",
MajorGridlineStyle = LineStyle.Dot, MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None MinorGridlineStyle = LineStyle.None
}); });
@@ -556,7 +554,8 @@ namespace MonitorModule.ViewModels
{ {
if (_systemConfig?.ConfigurationList == null) return; if (_systemConfig?.ConfigurationList == null) return;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(channel); string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, channel);
var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList(); var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList();
bool changed = false; bool changed = false;
@@ -650,7 +649,8 @@ namespace MonitorModule.ViewModels
{ {
if (args.Scope != TestStatus) return; if (args.Scope != TestStatus) return;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(args.Channel); string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
// 从 AvailableMethods 中移除 // 从 AvailableMethods 中移除
var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList(); var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList();
@@ -820,7 +820,7 @@ namespace MonitorModule.ViewModels
var recent = channel.DataPoints.ToArray(); var recent = channel.DataPoints.ToArray();
var startIdx = Math.Max(0, recent.Length - 10000); var startIdx = Math.Max(0, recent.Length - 10000);
for (int i = startIdx; i < recent.Length; i++) for (int i = startIdx; i < recent.Length; i++)
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue)); channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue));
Plot.Series.Add(channel.Series); Plot.Series.Add(channel.Series);
Plot.InvalidatePlot(true); Plot.InvalidatePlot(true);
@@ -844,7 +844,7 @@ namespace MonitorModule.ViewModels
channel.Series.Points.Clear(); channel.Series.Points.Clear();
var startIdx = Math.Max(0, points.Length - 10000); var startIdx = Math.Max(0, points.Length - 10000);
for (int i = startIdx; i < points.Length; i++) for (int i = startIdx; i < points.Length; i++)
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue)); channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue));
Plot.InvalidatePlot(true); Plot.InvalidatePlot(true);
} }
#endregion #endregion

View File

@@ -64,10 +64,15 @@ namespace MonitorModule.ViewModels
} }
/// <summary>查询日期null = 全部日期)</summary> /// <summary>查询日期null = 全部日期)</summary>
public DateTime? SelectedDate public DateTime? SelectedStartDate
{ {
get => _selectedDate; get => _selectedStartDate;
set => SetProperty(ref _selectedDate, value); set => SetProperty(ref _selectedStartDate, value);
}
public DateTime? SelectedEndDate
{
get => _selectedEndDate;
set => SetProperty(ref _selectedEndDate, value);
} }
public DataTable ResultTable public DataTable ResultTable
@@ -142,7 +147,8 @@ namespace MonitorModule.ViewModels
private string? _selectedMonitorName; private string? _selectedMonitorName;
private ObservableCollection<string> _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" }); private ObservableCollection<string> _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" });
private string? _selectedScope = "全部"; private string? _selectedScope = "全部";
private DateTime? _selectedDate; private DateTime? _selectedStartDate;
private DateTime? _selectedEndDate;
private DataTable _resultTable = new(); private DataTable _resultTable = new();
private string _statusMessage = "未连接"; private string _statusMessage = "未连接";
private int _pageIndex = 1; private int _pageIndex = 1;
@@ -257,7 +263,8 @@ namespace MonitorModule.ViewModels
{ {
SelectedMonitorName = null; SelectedMonitorName = null;
SelectedScope = "全部"; SelectedScope = "全部";
SelectedDate = null; SelectedStartDate = null;
SelectedEndDate = null;
PageIndex = 1; PageIndex = 1;
Query(); Query();
} }
@@ -274,8 +281,8 @@ namespace MonitorModule.ViewModels
var db = SqlSugarContext.DbContext; var db = SqlSugarContext.DbContext;
// 预计算日期范围,避免在表达式树中访问可空属性 // 预计算日期范围,避免在表达式树中访问可空属性
DateTime? dateStart = SelectedDate?.Date; DateTime? dateStart = SelectedStartDate?.Date;
DateTime? dateEnd = dateStart?.AddDays(1); DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
var query = db.Queryable<MonitorValueEntity>() var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName) .WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
@@ -317,8 +324,8 @@ namespace MonitorModule.ViewModels
{ {
var db = SqlSugarContext.DbContext; var db = SqlSugarContext.DbContext;
DateTime? dateStart = SelectedDate?.Date; DateTime? dateStart = SelectedStartDate?.Date;
DateTime? dateEnd = dateStart?.AddDays(1); DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
var query = db.Queryable<MonitorValueEntity>() var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName) .WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)

View File

@@ -52,6 +52,7 @@
<ColumnDefinition Width="150"/> <ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/> <ColumnDefinition Width="150"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
@@ -88,20 +89,25 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Margin="12,0,4,0"/> Margin="12,0,4,0"/>
<DatePicker Grid.Column="5" <DatePicker Grid.Column="5"
SelectedDate="{Binding SelectedDate}" SelectedDate="{Binding SelectedStartDate}"
materialDesign:HintAssist.Hint="全部" materialDesign:HintAssist.Hint="开始时间"
VerticalAlignment="Center"/>
<DatePicker Grid.Column="6"
Margin=" 5 0 0 0"
SelectedDate="{Binding SelectedEndDate}"
materialDesign:HintAssist.Hint="结束时间"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<!-- 按钮 --> <!-- 按钮 -->
<Button Grid.Column="6" <Button Grid.Column="7"
Content="查询" Content="查询"
Command="{Binding QueryCommand}" Command="{Binding QueryCommand}"
Padding="12,4" Margin="12,0,0,0"/> Padding="12,4" Margin="12,0,0,0"/>
<Button Grid.Column="7" <Button Grid.Column="8"
Content="清除" Content="清除"
Command="{Binding ClearFilterCommand}" Command="{Binding ClearFilterCommand}"
Padding="12,4" Margin="6,0,0,0"/> Padding="12,4" Margin="6,0,0,0"/>
<Button Grid.Column="8" <Button Grid.Column="9"
Content="导出 CSV" Content="导出 CSV"
Command="{Binding ExportCsvCommand}" Command="{Binding ExportCsvCommand}"
Padding="12,4" Margin="6,0,0,0"/> Padding="12,4" Margin="6,0,0,0"/>

View File

@@ -0,0 +1,109 @@
using Model;
using Model.Entity;
using Model.Models;
using ORM;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using static System.Formats.Asn1.AsnWriter;
namespace Service.Implement
{
public class TestReportService : BaseService<TestReportEntity>, ITestReportService
{
public TestReportService(SqlSugarRepository<TestReportEntity> repository) : base(repository)
{
}
/// <summary>
/// 根据 TestRoundId 获取符合条件的测试步骤
/// </summary>
public async Task<Result<List<TestReportEntity>>> GetByTestRoundIdAsync(Guid testRoundId)
{
try
{
var list = await _repository.Entities
.Where(x => x.TestRoundId == testRoundId)
.ToListAsync();
return Result<List<TestReportEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<TestReportEntity>>.Error("根据 TestRoundId 查询数据失败", ex);
}
}
/// <summary>
/// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间,
/// 支持按日期范围与台架名称筛选。
/// </summary>
public async Task<Result<IList<TestReportModel>>> GetFilterList(DateTime? startTime = null, DateTime? endTime = null, string? scope = null)
{
try
{
// 使用 SqlSugar 的 GroupBy 查询,聚合最大/最小时间
// 推荐采用这种按组投影的方式,将分组计算交由数据库处理,性能最佳
var query = _repository.Context.Queryable<TestReportEntity>();
// 日期范围筛选
if (startTime.HasValue)
query = query.Where(x => x.CreateTime >= startTime.Value);
if (endTime.HasValue)
query = query.Where(x => x.CreateTime <= endTime.Value);
// 台架名称筛选
if (!string.IsNullOrEmpty(scope))
query = query.Where(x => x.Scope == scope);
var list = await query
.GroupBy(x => x.TestRoundId)
.Select(x => new TestReportModel
{
TestRoundId = x.TestRoundId,
StartTime = SqlFunc.AggregateMin(x.CreateTime),
EndTime = SqlFunc.AggregateMax(x.CreateTime),
Scope = x.Scope,
FileName = x.FileName,
})
.ToListAsync();
return Result<IList<TestReportModel>>.Success(list);
}
catch (Exception ex)
{
return Result<IList<TestReportModel>>.Error("获取分组过滤列表失败", ex);
}
}
/// <summary>
/// 根据日期和台架名称筛选获取完整的测试步骤实体列表(无分组,用于导出)
/// </summary>
public async Task<Result<List<TestReportEntity>>> GetEntitiesByFilter(DateTime? startTime = null, DateTime? endTime = null, string? scope = null)
{
try
{
var query = _repository.Context.Queryable<TestReportEntity>();
if (startTime.HasValue)
query = query.Where(x => x.CreateTime >= startTime.Value);
if (endTime.HasValue)
query = query.Where(x => x.CreateTime <= endTime.Value);
if (!string.IsNullOrEmpty(scope))
query = query.Where(x => x.Scope == scope);
var list = await query
.OrderBy(x => x.CreateTime)
.ToListAsync();
return Result<List<TestReportEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<TestReportEntity>>.Error("获取导出数据失败", ex);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using Model;
using Model.Entity;
using Model.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Service.Interface
{
public interface ITestReportService:IBaseService<TestReportEntity>
{
/// <summary>
/// 根据TestRoundId获符合的测试步骤
/// </summary>
/// <param name="TestRoundId">TestRoundId</param>
/// <returns>返回符合的TestReportEntity列表</returns>
Task<Result<List<TestReportEntity>>> GetByTestRoundIdAsync(Guid TestRoundId);
/// <summary>
/// 根据 TestRoundId 分组,获取唯一记录并按分组统计最大与最小时间
/// </summary>
/// <param name="startTime">开始日期筛选(可选)</param>
/// <param name="endTime">结束日期筛选(可选)</param>
/// <param name="scope">台架名称筛选(可选)</param>
Task<Result<IList<TestReportModel>>> GetFilterList(DateTime? startTime = null, DateTime? endTime = null, string? scope = null);
/// <summary>
/// 根据日期和台架名称筛选获取完整的测试步骤实体列表(无分组,用于导出)
/// </summary>
/// <param name="startTime">开始日期筛选(可选)</param>
/// <param name="endTime">结束日期筛选(可选)</param>
/// <param name="scope">台架名称筛选(可选)</param>
Task<Result<List<TestReportEntity>>> GetEntitiesByFilter(DateTime? startTime = null, DateTime? endTime = null, string? scope = null);
}
}

View File

@@ -402,11 +402,11 @@
<Border Background="White" <Border Background="White"
BorderBrush="#E0E0E0" BorderThickness="1" BorderBrush="#E0E0E0" BorderThickness="1"
CornerRadius="4" Padding="14"> CornerRadius="4" Padding="14">
<StackPanel> <!--<StackPanel>
<TextBlock Text="高级设置" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/> <TextBlock Text="高级设置" FontWeight="Bold" FontSize="14" Margin="0,0,0,10"/>
<TextBlock Foreground="#888" FontSize="12" TextWrapping="Wrap" <TextBlock Foreground="#888" FontSize="12" TextWrapping="Wrap"
Text="超时、重试、缓存策略等高级选项后续在此扩展。"/> Text="超时、重试、缓存策略等高级选项后续在此扩展。"/>
</StackPanel> </StackPanel>-->
</Border> </Border>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>

View File

@@ -333,7 +333,7 @@ namespace TestingModule.ViewModels
foreach (var type in validTypes) foreach (var type in validTypes)
{ {
//拦截没有在设备列表中的设备类型 //拦截没有在设备列表中的设备类型
//if (_systemConfig.DeviceList.Where(x=>x.Remark==type.Name).ToList().Count==0 && type.FullName.Contains("DeviceCommand.Device")) //if (_systemConfig.DeviceList.Where(x=>x.Remark==type.Name).ToList().Count==0 && type.FullName.Contains("DeviceCommand.Devices"))
//{ //{
// continue; // continue;
//} //}

View File

@@ -93,7 +93,20 @@ namespace TestingModule.ViewModels.Dialogs
var index = Program.Parameters var index = Program.Parameters
.Select((x, i) => new { x, i }) .Select((x, i) => new { x, i })
.FirstOrDefault(p => p.x.ID == _ScopedContext.SelectedParameter.ID)?.i; .FirstOrDefault(p => p.x.ID == _ScopedContext.SelectedParameter.ID)?.i;
if (!_ScopedContext.SelectedParameter.IsEditable)
{
//公共变量只允许更改ParameterCategory为Input,Temp
if (Program.Parameters[index.Value].Value != Parameter.Value
|| Program.Parameters[index.Value].UpperLimit != Parameter.UpperLimit
|| Program.Parameters[index.Value].LowerLimit != Parameter.LowerLimit
|| Program.Parameters[index.Value].Name != Parameter.Name
|| Parameter.Category == ParameterCategory.Output
|| Program.Parameters[index.Value].Type != Parameter.Type)
{
RequestClose.Invoke(ButtonResult.Yes);
return;
}
}
if (index.HasValue) if (index.HasValue)
{ {
Program.Parameters[index.Value] = Parameter; Program.Parameters[index.Value] = Parameter;

View File

@@ -127,26 +127,37 @@ namespace TestingModule.ViewModels
if (SelectedDevice == null) return; if (SelectedDevice == null) return;
var type = SelectedDevice.DeviceType.Split('.').Last(); var type = SelectedDevice.DeviceType.Split('.').Last();
var viewName = type + "View"; var viewName = type + "View";
var vmName = type + "ViewModel"; // 按照命名规范拼接出对应的 VM 类型名
try try
{ {
// 1. 先确保弹窗管理器窗口已打开(首次 Show后续只追加 Tab // 1. 先确保弹窗管理器窗口已打开
if (_dialogWindow == null || !_dialogWindow.IsVisible) if (_dialogWindow == null || !_dialogWindow.IsVisible)
{ {
_dialogService.Show("DialogMangerView"); _dialogService.Show("DialogMangerView");
// 找到刚刚被 DialogService 打开的窗口(按 DataContext 类型名匹配)
_dialogWindow = System.Windows.Application.Current.Windows _dialogWindow = System.Windows.Application.Current.Windows
.OfType<System.Windows.Window>() .OfType<System.Windows.Window>()
.FirstOrDefault(w => w.DataContext?.GetType().Name == "DialogMangerViewModel"); .FirstOrDefault(w => w.DataContext?.GetType().Name == "DialogMangerViewModel");
} }
// 2. 从容器按注册名解析设备编辑 View // 2. 从当前专属容器解析设备编辑 View 实例
var view = _containerProvider.Resolve<object>(viewName) as System.Windows.FrameworkElement; var view = _containerProvider.Resolve<object>(viewName) as System.Windows.FrameworkElement;
if (view == null) return; if (view == null) return;
// 3. 通过反射调用 ViewModel 上的 Initialize(deviceName) 方法, // 3. 关键:从同一个台架的专属容器中解析出该设备对应的 ViewModel 实例
// 避免 TestingModule 直接引用 DeviceEditModule 的类型 // 从而实现:不同的台架(不同的专属 _containerProvider解析出各自独立的 VM 实例
var vmType = Assembly.Load("DeviceEditModule").GetType($"DeviceEditModule.ViewModels.{vmName}");
if (vmType != null)
{
var scopedViewModel = _containerProvider.Resolve(vmType);
if (scopedViewModel != null)
{
// 手动绑定 DataContext将其锁死在当前作用域内
view.DataContext = scopedViewModel;
}
}
// 4. 调用 ViewModel 上的 Initialize 方法
var vm = view.DataContext; var vm = view.DataContext;
if (vm != null) if (vm != null)
{ {
@@ -154,9 +165,7 @@ namespace TestingModule.ViewModels
initMethod?.Invoke(vm, new object[] { SelectedDevice.DeviceName }); initMethod?.Invoke(vm, new object[] { SelectedDevice.DeviceName });
} }
// 4. 发布事件 → DialogMangerViewModel 接收后将此 View 添加为 Tab // 5. 发布事件,将绑定好独立 VM 的 View 实例作为 Tab 载入
// 标题格式:{当前作用域} - {设备名称}
// 去重依据:设备硬件指纹(同一物理设备不重复打开)
var fingerprint = DeviceManager.ExtractHardwareFingerprint(SelectedDevice); var fingerprint = DeviceManager.ExtractHardwareFingerprint(SelectedDevice);
_eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo _eventAggregator.GetEvent<AddDialogTabEvent>().Publish(new DialogTabInfo
{ {
@@ -165,7 +174,7 @@ namespace TestingModule.ViewModels
Content = view Content = view
}); });
// 5. 窗口置顶(确保用户看到新增的 Tab // 6. 窗口置顶
if (_dialogWindow != null && _dialogWindow.IsVisible) if (_dialogWindow != null && _dialogWindow.IsVisible)
{ {
if (_dialogWindow.WindowState == System.Windows.WindowState.Minimized) if (_dialogWindow.WindowState == System.Windows.WindowState.Minimized)
@@ -181,7 +190,7 @@ namespace TestingModule.ViewModels
private void ParameterDelete() private void ParameterDelete()
{ {
if (!_globalInfo.IsAdmin) return; if (!_globalInfo.IsAdmin || !SelectedParameter.IsEditable) return;
Program.Parameters.Remove(SelectedParameter); Program.Parameters.Remove(SelectedParameter);
} }
@@ -198,10 +207,11 @@ namespace TestingModule.ViewModels
if (r.Result == ButtonResult.OK) if (r.Result == ButtonResult.OK)
{ {
_eventAggregator.GetEvent<ParamsChangedEvent>().Publish(); _eventAggregator.GetEvent<ParamsChangedEvent>().Publish();
ShowInfoMessageBox("保存成功", () => { });
} }
else else if (r.Result == ButtonResult.Yes)
{ {
ShowErrorMessageBox("公共变量只能设置变量类型Input,Temp", ()=>{ });
} }
}); });

View File

@@ -4,6 +4,8 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
@@ -79,6 +81,10 @@ namespace TestingModule.ViewModels
private readonly GlobalInfo _globalInfo; private readonly GlobalInfo _globalInfo;
private List<StepVM> tmpCopyList = new List<StepVM>(); private List<StepVM> tmpCopyList = new List<StepVM>();
// 追踪当前订阅的集合实例,用于在集合被整体替换时重新订阅
private ObservableCollection<StepVM> _trackedStepCollection;
private ObservableCollection<StepVM> _trackedErrorStepCollection;
#endregion #endregion
#region #region
@@ -95,19 +101,16 @@ namespace TestingModule.ViewModels
_ScopedContext = scopedContext; _ScopedContext = scopedContext;
_systemConfig = systemConfig; _systemConfig = systemConfig;
_globalInfo = globalInfo; _globalInfo = globalInfo;
Title = _systemConfig.Title;
EditStepCommand = new DelegateCommand(EditStep); EditStepCommand = new DelegateCommand(EditStep);
CopyStepCommand = new DelegateCommand(CopyStep); CopyStepCommand = new DelegateCommand(CopyStep);
PasteStepCommand = new DelegateCommand(PasteStep); PasteStepCommand = new DelegateCommand(PasteStep);
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);
Program.StepCollection.CollectionChanged += StepCollection_CollectionChanged; SubscribeStepCollections();
Program.ErrorStepCollection.CollectionChanged += StepCollection_CollectionChanged; Program.PropertyChanged += Program_PropertyChanged;
Admin = _globalInfo.IsAdmin; Admin = _globalInfo.IsAdmin;
_eventAggregator.GetEvent<AlarmEvent>().Subscribe(() =>
{
SelectedTabIndex = 1;
});
} }
private void SelectionChanged(object parameter) private void SelectionChanged(object parameter)
@@ -206,13 +209,67 @@ namespace TestingModule.ViewModels
#endregion #endregion
#region #region
private void StepCollection_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
/// <summary>
/// 订阅当前 Program 的 StepCollection 和 ErrorStepCollection 的 CollectionChanged 事件。
/// </summary>
private void SubscribeStepCollections()
{
_trackedStepCollection = Program.StepCollection;
_trackedErrorStepCollection = Program.ErrorStepCollection;
if (_trackedStepCollection != null)
_trackedStepCollection.CollectionChanged += StepCollection_CollectionChanged;
if (_trackedErrorStepCollection != null)
_trackedErrorStepCollection.CollectionChanged += StepCollection_CollectionChanged;
}
/// <summary>
/// 取消订阅当前追踪的集合事件。
/// </summary>
private void UnsubscribeStepCollections()
{
if (_trackedStepCollection != null)
{
_trackedStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
_trackedStepCollection = null;
}
if (_trackedErrorStepCollection != null)
{
_trackedErrorStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
_trackedErrorStepCollection = null;
}
}
/// <summary>
/// 当 Program 的 StepCollection / ErrorStepCollection 被整体替换时,自动重新订阅并刷新序号。
/// </summary>
private void Program_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ProgramVM.StepCollection) ||
e.PropertyName == nameof(ProgramVM.ErrorStepCollection))
{
UnsubscribeStepCollections();
SubscribeStepCollections();
// 集合被整体替换后,对新集合重新编号
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
{
for (int i = 0; i < Program.StepCollection.Count; i++)
Program.StepCollection[i].Index = i + 1;
for (int i = 0; i < Program.ErrorStepCollection.Count; i++)
Program.ErrorStepCollection[i].Index = i + 1;
}), System.Windows.Threading.DispatcherPriority.Background);
}
}
private void StepCollection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
var collection = sender as ObservableCollection<StepVM>; var collection = sender as ObservableCollection<StepVM>;
// Add/Move/Remove 都会触发,这里判断具体情形 // Add/Move/Remove 都会触发,这里判断具体情形
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add || if (e.Action == NotifyCollectionChangedAction.Add ||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Move|| e.Action == NotifyCollectionChangedAction.Move||
e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove) e.Action == NotifyCollectionChangedAction.Remove)
{ {
// 如果需要等 UI 更新完再处理,可也用 Dispatcher 延迟一小段时间 // 如果需要等 UI 更新完再处理,可也用 Dispatcher 延迟一小段时间
Application.Current?.Dispatcher.BeginInvoke(new Action(() => Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
@@ -238,31 +295,27 @@ namespace TestingModule.ViewModels
{ {
try try
{ {
// 1. 【核心修复】必须取消订阅 CollectionChanged 事件,否则 VM 永远无法被释放 // 1. 取消订阅 CollectionChanged 事件(从追踪的实例上取消,而非可能已被替换的当前实例)
UnsubscribeStepCollections();
// 2. 取消订阅 Program.PropertyChanged
if (Program != null) if (Program != null)
{ {
if (Program.StepCollection != null) Program.PropertyChanged -= Program_PropertyChanged;
{
Program.StepCollection.CollectionChanged -= StepCollection_CollectionChanged;
}
if (Program.ErrorStepCollection != null)
{
Program.ErrorStepCollection.CollectionChanged -= StepCollection_CollectionChanged;
}
} }
// 2. 【核心修复】必须显式退订 Prism 全局事件AlarmEvent // 3. 【核心修复】必须显式退订 Prism 全局事件AlarmEvent
// 注意:因为订阅时使用的是匿名 Lambda最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销 // 注意:因为订阅时使用的是匿名 Lambda最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null); _eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
// 3. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针 // 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
tmpCopyList?.Clear(); tmpCopyList?.Clear();
tmpCopyList = null!; tmpCopyList = null!;
SelectedItems?.Clear(); SelectedItems?.Clear();
SelectedItems = null!; SelectedItems = null!;
// 4. 清除选中项状态引用 // 5. 清除选中项状态引用
SelectedStep = null; SelectedStep = null;
if (_ScopedContext != null) if (_ScopedContext != null)
{ {

View File

@@ -12,7 +12,7 @@
d:DesignWidth="800" d:DesignWidth="800"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid> <Grid>
<GroupBox Header="程序编辑区域"> <GroupBox Header="{Binding Title}">
<TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}"> <TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
<i:Interaction.Behaviors> <i:Interaction.Behaviors>
<behaviors:TabControlSelectionChangedBehavior Command="{Binding TabSelectionChangedCommand}" <behaviors:TabControlSelectionChangedBehavior Command="{Binding TabSelectionChangedCommand}"

View File

@@ -13,7 +13,9 @@ namespace UIShare.Converters // 确保命名空间跟你项目一致
if (double.TryParse(parameter.ToString(), out double targetWidth)) if (double.TryParse(parameter.ToString(), out double targetWidth))
{ {
// 如果实际宽度 小于 设定的阈值比如600返回 True // 如果实际宽度 小于 设定的阈值比如600返回 True
return actualWidth < targetWidth; //return actualWidth < targetWidth;
//工控机屏幕较大直接给1000
return actualWidth < 1000;
} }
} }
return false; return false;

View File

@@ -69,7 +69,7 @@ namespace UIShare.GlobalVariable
{ {
if (_disposed) return; if (_disposed) return;
string signalFingerprint = BuildFingerprint(channel); string signalFingerprint = BuildFingerprint(canFingerprint, channel);
var now = DateTime.Now; var now = DateTime.Now;
// 获取引用该 CAN 设备的所有作用域 // 获取引用该 CAN 设备的所有作用域
@@ -96,7 +96,11 @@ namespace UIShare.GlobalVariable
Time = now Time = now
}); });
ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo); string MonitorStatus = ValueLimitAlarmHelper.CheckAlarm(scope, signalFingerprint, methodName, physicalValue, _globalInfo);
if (MonitorStatus != "" && MonitorStatus != "未报警")
{
_eventAggregator.GetEvent<AlarmEvent>().Publish((scope,canFingerprint, MonitorStatus));
}
} }
} }
} }
@@ -129,11 +133,12 @@ namespace UIShare.GlobalVariable
} }
/// <summary> /// <summary>
/// 生成 CAN 信号的 Fingerprint 格式:"CAN:{Channel}" /// 生成 CAN 信号的 Fingerprint 格式:"{canDeviceFingerprint}:{channel}"
/// canDeviceFingerprint 来自 DeviceManager.ExtractHardwareFingerprint如 "CAN:0"
/// </summary> /// </summary>
public static string BuildFingerprint(uint channel) public static string BuildFingerprint(string canDeviceFingerprint, uint channel)
{ {
return $"CAN:{channel}"; return $"{canDeviceFingerprint}:{channel}";
} }
public void Dispose() public void Dispose()

View File

@@ -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>
/// 保存指定的配置实例 /// 保存指定的配置实例

View File

@@ -1,4 +1,5 @@
using DeviceCommand.Base; using DeviceCommand.Base;
using DeviceCommand.Devices;
using Logger; using Logger;
using Model.Models; using Model.Models;
using Prism.Events; using Prism.Events;
@@ -34,6 +35,7 @@ namespace UIShare.GlobalVariable
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary> /// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap(); private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
public ZLGCANFD CANFD { get; set; } public ZLGCANFD CANFD { get; set; }
public IOBoardGroup IOGroup { get; set; }
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo, IEventAggregator eventAggregator) public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo, IEventAggregator eventAggregator)
{ {
@@ -71,6 +73,20 @@ namespace UIShare.GlobalVariable
return string.Empty; return string.Empty;
} }
/// <summary>
/// 获取当前作用域配置的 CAN 设备硬件指纹(与 GlobalInfo.CanPool 的 Key 一致)。
/// 如 "CAN:0",其中 0 是 CANConfig.DeviceIndex。
/// </summary>
public string GetCanDeviceFingerprint()
{
if (_systemConfig?.DeviceList == null) return string.Empty;
var canConfig = _systemConfig.DeviceList.FirstOrDefault(d =>
d != null && d.IsEnabled &&
string.Equals(d.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase));
return canConfig != null ? ExtractHardwareFingerprint(canConfig) : string.Empty;
}
private void InitDevices() private void InitDevices()
{ {
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase); DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
@@ -193,6 +209,25 @@ namespace UIShare.GlobalVariable
LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{config.DeviceName}] 实例化失败:{inner}"); LoggerHelper.ErrorWithNotify(_scopeName, $"设备 [{config.DeviceName}] 实例化失败:{inner}");
} }
} }
// IOGroup 初始化:从已实例化的设备中取出前两个 IOBoard 实例组建 IOBoardGroup
try
{
var ioBoards = DeviceMap.Values.OfType<IOBoard>().Take(2).ToList();
if (ioBoards.Count == 2)
{
IOGroup = new IOBoardGroup(ioBoards[0], ioBoards[1]);
LoggerHelper.Info($"IOBoardGroup 已初始化Board1={ioBoards[0].GetType().Name}, Board2={ioBoards[1].GetType().Name}。");
}
else
{
LoggerHelper.Warn($"IOBoardGroup 初始化跳过:需要 2 个 IOBoard 实例,当前仅找到 {ioBoards.Count} 个。");
}
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(_scopeName, $"IOBoardGroup 初始化失败:{ex.Message}");
}
} }
public async Task ConnectAllDevices(CancellationToken ct = default) public async Task ConnectAllDevices(CancellationToken ct = default)
@@ -393,13 +428,17 @@ namespace UIShare.GlobalVariable
private static async Task<bool> ConnectTcpAsync(string name, IBaseInterface device, CancellationToken ct) private static async Task<bool> ConnectTcpAsync(string name, IBaseInterface device, CancellationToken ct)
{ {
if (device is not ITcp tcp) if (device is ITcp tcp)
{ {
return await tcp.ConnectAsync(ct);
}
if (device is IModbusDevice modbusTCP)
{
return await modbusTCP.ConnectAsync(ct);
}
LoggerHelper.Warn($"设备 [{name}] 配置为 Tcp 但未实现 ITcp实际类型为 {device.GetType().Name}。"); LoggerHelper.Warn($"设备 [{name}] 配置为 Tcp 但未实现 ITcp实际类型为 {device.GetType().Name}。");
return false; return false;
} }
return await tcp.ConnectAsync(ct);
}
private static async Task<bool> ConnectSerialAsync(string name, IBaseInterface device, CancellationToken ct) private static async Task<bool> ConnectSerialAsync(string name, IBaseInterface device, CancellationToken ct)
{ {

View File

@@ -181,7 +181,11 @@ namespace UIShare.GlobalVariable
}); });
// 同步检查该作用域 ValueLimitList 是否超限 // 同步检查该作用域 ValueLimitList 是否超限
ValueLimitAlarmHelper.CheckAlarm(scope, entry.Fingerprint, entry.MethodName, value, _globalInfo); string MonitorStatus= ValueLimitAlarmHelper.CheckAlarm(scope, entry.Fingerprint, entry.MethodName, value, _globalInfo);
if (MonitorStatus != "" && MonitorStatus != "未报警")
{
_eventAggregator.GetEvent<AlarmEvent>().Publish((scope,entry.Fingerprint, MonitorStatus));
}
} }
} }
catch catch

View File

@@ -3,6 +3,8 @@ using UIShare.PubEvent;
using Common.Tools; using Common.Tools;
using Logger; using Logger;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Model.Entity;
using Service.Interface;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
@@ -26,6 +28,7 @@ namespace UIShare
//private Devices _devices; //private Devices _devices;
private IContainerProvider containerProvider; private IContainerProvider containerProvider;
private IEventAggregator _eventAggregator; private IEventAggregator _eventAggregator;
private ITestReportService _testReportService;
private readonly Dictionary<Guid, ParameterVM> tmpParameters = []; private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
@@ -42,13 +45,14 @@ namespace UIShare
/// <summary>标记是否已被注销,执行方法据此安全退出</summary> /// <summary>标记是否已被注销,执行方法据此安全退出</summary>
private volatile bool _disposed = false; private volatile bool _disposed = false;
private Guid TestRoundID; public Guid TestRoundID;
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager) public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService)
{ {
_scopedContext = ScopedContext; _scopedContext = ScopedContext;
_systemConfig = systemConfig; _systemConfig = systemConfig;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_deviceManager= deviceManager; _deviceManager= deviceManager;
_testReportService = testReportService;
//_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)
@@ -114,6 +118,7 @@ namespace UIShare
step.CurrentLoopCount = context.LoopCount; step.CurrentLoopCount = context.LoopCount;
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始,共{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始,共{context.LoopCount}次", depth);
index++; index++;
await SaveStepRecordAsync(step, depth, true);
} }
// 处理循环结束 // 处理循环结束
@@ -124,6 +129,7 @@ namespace UIShare
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth: depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth: depth);
step.Result = 2; step.Result = 2;
index++; index++;
await SaveStepRecordAsync(step, depth, true);
continue; continue;
} }
@@ -138,6 +144,7 @@ namespace UIShare
// 继续循环:跳转到循环开始后的第一条指令 // 继续循环:跳转到循环开始后的第一条指令
index = context.StartIndex + 1; index = context.StartIndex + 1;
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
await SaveStepRecordAsync(step, depth, true);
} }
else else
{ {
@@ -154,6 +161,7 @@ namespace UIShare
program.ErrorStepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1; program.ErrorStepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1;
loopStopwatchStack.Pop(); loopStopwatchStack.Pop();
} }
await SaveStepRecordAsync(step, depth, true);
} }
} }
@@ -212,6 +220,7 @@ namespace UIShare
stepStopwatch.Stop(); stepStopwatch.Stop();
step.RunTime = (int)stepStopwatch.ElapsedMilliseconds; step.RunTime = (int)stepStopwatch.ElapsedMilliseconds;
} }
await SaveStepRecordAsync(step, depth, true);
} }
} }
@@ -284,6 +293,7 @@ namespace UIShare
step.CurrentLoopCount = context.LoopCount; step.CurrentLoopCount = context.LoopCount;
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始({step.Name}),共{context.LoopCount}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环开始({step.Name}),共{context.LoopCount}次", depth);
index++; index++;
await SaveStepRecordAsync(step, depth, false);
} }
// 处理循环结束 // 处理循环结束
@@ -294,6 +304,7 @@ namespace UIShare
LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth:depth); LoggerHelper.ErrorWithNotify(_systemConfig.Title, "未匹配的循环结束指令", depth:depth);
step.Result = 2; step.Result = 2;
index++; index++;
await SaveStepRecordAsync(step, depth, false);
continue; continue;
} }
@@ -308,6 +319,7 @@ namespace UIShare
// 继续循环:跳转到循环开始后的第一条指令 // 继续循环:跳转到循环开始后的第一条指令
index = context.StartIndex + 1; index = context.StartIndex + 1;
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth); LoggerHelper.InfoWithNotify(_systemConfig.Title, $"循环第{context.CurrentLoop}次结束,跳回开始,剩余{context.LoopCount - context.CurrentLoop}次", depth);
await SaveStepRecordAsync(step, depth, false);
} }
else else
{ {
@@ -324,6 +336,7 @@ namespace UIShare
program.StepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1; program.StepCollection.First(x => x.ID == step.LoopStartStepId).Result = 1;
loopStopwatchStack.Pop(); loopStopwatchStack.Pop();
} }
await SaveStepRecordAsync(step, depth, false);
} }
} }
@@ -389,6 +402,7 @@ namespace UIShare
_scopedContext.SingleStep = false; _scopedContext.SingleStep = false;
_eventAggregator.GetEvent<RunSingalCompletedEvent>().Publish("Play"); _eventAggregator.GetEvent<RunSingalCompletedEvent>().Publish("Play");
} }
await SaveStepRecordAsync(step, depth, false);
} }
} }
@@ -568,6 +582,10 @@ namespace UIShare
{ {
instance = _deviceManager.CANFD; instance = _deviceManager.CANFD;
} }
else if (targetType.Name == "IOBoardGroup")
{
instance = _deviceManager.IOGroup;
}
else instance = _deviceManager.DeviceMap[targetType.Name]; else instance = _deviceManager.DeviceMap[targetType.Name];
} }
catch (Exception ex) catch (Exception ex)
@@ -665,6 +683,61 @@ namespace UIShare
} }
} }
/// <summary>
/// 将单个步骤的执行结果保存到数据库(测试报告)。
/// 同一次运行的所有步骤共享 TestRoundID导出时按此 Guid 查询。
/// </summary>
private async Task SaveStepRecordAsync(StepVM step, int depth, bool isErrorStep)
{
try
{
// 获取输出参数值
string? outputValue = null;
if (step.Method != null)
{
var outputParam = step.Method.Parameters.FirstOrDefault(p => p.Category == ParameterCategory.Output);
if (outputParam?.Value != null)
outputValue = outputParam.Value.ToString();
}
var entity = new TestReportEntity
{
TestRoundId = TestRoundID,
Scope = _systemConfig.Title,
FileName = _systemConfig.CurrentADPFile ?? "",
StepIndex = step.Index,
StepName = step.Name ?? "",
StepType = step.StepType ?? "普通步骤",
MethodName = step.Method?.Name,
MethodFullName = step.Method?.FullName,
Result = step.Result switch
{
-1 => "未执行",
0 => "执行中",
1 => "成功",
2 => "失败",
_ => "未知"
},
RunTimeMs = step.RunTime,
OutputValue = outputValue,
Depth = depth,
IsErrorStep = isErrorStep,
LoopRemaining = step.CurrentLoopCount,
CreateTime = DateTime.Now
};
var result = await _testReportService.InsertAsync(entity);
if (!result.IsSuccess)
{
LoggerHelper.Error($"保存步骤记录失败 [{step.Index}]: {result.Msg}");
}
}
catch (Exception ex)
{
LoggerHelper.Error($"保存步骤记录失败 [{step.Index}]: {ex.Message}");
}
}
public void ResetAllStepStatus(ObservableCollection<StepVM> StepCollection) public void ResetAllStepStatus(ObservableCollection<StepVM> StepCollection)
{ {
foreach (var step in StepCollection) foreach (var step in StepCollection)

View File

@@ -24,6 +24,7 @@ namespace UIShare.GlobalVariable
public string TSMasterName { get; set; } = "ADP测试上位机"; public string TSMasterName { get; set; } = "ADP测试上位机";
public string SubProgramFilePath { get; set; } = @"D:\ADP\子程序\"; public string SubProgramFilePath { get; set; } = @"D:\ADP\子程序\";
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
public string CurrentADPFile { get; set; }
public int PerformanceLevel { get; set; } = 50; public int PerformanceLevel { get; set; } = 50;
public string DefaultProgramFilePath { get; set; } = ""; public string DefaultProgramFilePath { get; set; } = "";
public string DefaultBLFFilePath { get; set; } = ""; public string DefaultBLFFilePath { get; set; } = "";
@@ -47,15 +48,25 @@ namespace UIShare.GlobalVariable
{ {
Category = ParameterCategory.Input, Category = ParameterCategory.Input,
Type = typeof(int), Type = typeof(int),
Name = "继电器占位1", Name = "台架序号",
Value = 0, Value = 1,
IsEditable=false
}, },
new ParameterVM new ParameterVM
{ {
Category = ParameterCategory.Input, Category = ParameterCategory.Input,
Type = typeof(int), Type = typeof(int),
Name = "继电器占位2", Name = "直流负载通道",
Value = 1, Value = 1,
IsEditable=false
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "交流电源通道",
Value = 1,
IsEditable=false
}, },
new ParameterVM new ParameterVM
{ {
@@ -63,13 +74,23 @@ namespace UIShare.GlobalVariable
Type = typeof(int), Type = typeof(int),
Name = "CAN通道", Name = "CAN通道",
Value = 0, Value = 0,
IsEditable=false
}, },
new ParameterVM new ParameterVM
{ {
Category = ParameterCategory.Input, Category = ParameterCategory.Input,
Type = typeof(int), Type = typeof(int),
Name = "示波器通道", Name = "示波器通道1",
Value = 0, Value = 0,
IsEditable=false
},
new ParameterVM
{
Category = ParameterCategory.Input,
Type = typeof(int),
Name = "示波器通道2",
Value = 0,
IsEditable=false
}, },
new ParameterVM new ParameterVM
{ {
@@ -77,6 +98,7 @@ namespace UIShare.GlobalVariable
Type = typeof(int), Type = typeof(int),
Name = "功率分析仪通道1", Name = "功率分析仪通道1",
Value = 0, Value = 0,
IsEditable=false
}, },
new ParameterVM new ParameterVM
{ {
@@ -84,6 +106,7 @@ namespace UIShare.GlobalVariable
Type = typeof(int), Type = typeof(int),
Name = "功率分析仪通道2", Name = "功率分析仪通道2",
Value = 0, Value = 0,
IsEditable=false
}, },
}; };
// public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new() // public ObservableCollection<DeviceInfoVM> DeviceList { get; set; } = new()

View File

@@ -16,26 +16,16 @@ namespace UIShare.GlobalVariable
/// <param name="methodName">方法名/信号标识</param> /// <param name="methodName">方法名/信号标识</param>
/// <param name="value">当前采样值</param> /// <param name="value">当前采样值</param>
/// <param name="globalInfo">全局信息</param> /// <param name="globalInfo">全局信息</param>
public static void CheckAlarm(string scope, string fingerprint, string methodName, double value, GlobalInfo globalInfo) public static string CheckAlarm(string scope, string fingerprint, string methodName, double value, GlobalInfo globalInfo)
{ {
if (globalInfo?.ConfigDic == null) return; if (globalInfo?.ConfigDic == null) return "";
if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return; if (!globalInfo.ConfigDic.TryGetValue(scope, out var systemConfig)) return "";
if (systemConfig.ValueLimitList == null) return; if (systemConfig.ValueLimitList == null) return "";
var limit = systemConfig.ValueLimitList.FirstOrDefault(x => var limit = systemConfig.ValueLimitList.FirstOrDefault(x =>
x.Fingerprint == fingerprint && x.MethodName == methodName); x.Fingerprint == fingerprint && x.MethodName == methodName);
if (limit == null) return; if (limit == null) return "";
if (value > limit.Upper) if (value > limit.UpperExtreme)
{
limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.;
}
else if (value < limit.Lower)
{
limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.;
}
else if(value > limit.UpperExtreme)
{ {
limit.IsAlarm = true; limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.; limit.AlarmSatus = AlarmStatus.;
@@ -45,12 +35,22 @@ namespace UIShare.GlobalVariable
limit.IsAlarm = true; limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.; limit.AlarmSatus = AlarmStatus.;
} }
else if (value > limit.Upper)
{
limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.;
}
else if (value < limit.Lower)
{
limit.IsAlarm = true;
limit.AlarmSatus = AlarmStatus.;
}
else else
{ {
limit.IsAlarm = false; limit.IsAlarm = false;
limit.AlarmSatus = AlarmStatus.; limit.AlarmSatus = AlarmStatus.;
} }
return limit.AlarmSatus.ToString();
} }
} }
} }

View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace UIShare.PubEvent namespace UIShare.PubEvent
{ {
public class AlarmEvent :PubSubEvent public class AlarmEvent : PubSubEvent<(string Scope, string Fingerprint, string Status)>
{ {
} }
} }

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace UIShare.PubEvent
{
public class SilenceBuzzerEvent:PubSubEvent<string>
{
}
}

View File

@@ -21,6 +21,8 @@
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" /> <ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\Logger\Logger.csproj" /> <ProjectReference Include="..\Logger\Logger.csproj" />
<ProjectReference Include="..\ORM\ORM.csproj" />
<ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" /> <ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -10,5 +10,6 @@ namespace UIShare.UIViewModel
public string SignalName { get; set; } public string SignalName { get; set; }
public int CollectionInterval { get; set; } public int CollectionInterval { get; set; }
public Guid CollectionID { get; set; } public Guid CollectionID { get; set; }
public string MethodName {get;set;}
} }
} }

View File

@@ -1,5 +1,6 @@
using OxyPlot.Series; using OxyPlot.Series;
using OxyPlot; using OxyPlot;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Prism.Mvvm; using Prism.Mvvm;
using NCalc; using NCalc;
@@ -52,7 +53,7 @@ namespace UIShare.UIViewModel
// ===== 数据存储 ===== // ===== 数据存储 =====
/// <summary>所有采样数据点(线程安全),与 OxyPlot 无关</summary> /// <summary>所有采样数据点(线程安全),与 OxyPlot 无关</summary>
public ConcurrentQueue<(double Time, double RawValue, double DisplayValue)> DataPoints { get; } = new(); public ConcurrentQueue<(DateTime Time, double RawValue, double DisplayValue)> DataPoints { get; } = new();
/// <summary>最大缓冲数据点数,超出则丢弃最旧的</summary> /// <summary>最大缓冲数据点数,超出则丢弃最旧的</summary>
public int MaxBuffer { get; set; } = 5000; public int MaxBuffer { get; set; } = 5000;
@@ -67,7 +68,7 @@ namespace UIShare.UIViewModel
// ===== 辅助方法 ===== // ===== 辅助方法 =====
/// <summary>记录一个数据点RawValue 经数学变换后得到 DisplayValue一并入队</summary> /// <summary>记录一个数据点RawValue 经数学变换后得到 DisplayValue一并入队</summary>
public void Record(double time, double rawValue) public void Record(DateTime time, double rawValue)
{ {
double displayValue = rawValue; double displayValue = rawValue;
@@ -82,8 +83,8 @@ namespace UIShare.UIViewModel
// 如果正在显示,同步到 Series // 如果正在显示,同步到 Series
if (_series != null) if (_series != null)
{ {
_series.Points.Add(new DataPoint(time, displayValue)); _series.Points.Add(new DataPoint(time.ToOADate(), displayValue));
while (_series.Points.Count > 200) while (_series.Points.Count > 10000)
{ {
_series.Points.RemoveAt(0); _series.Points.RemoveAt(0);
} }

View File

@@ -5,6 +5,8 @@ using System.Collections.Generic;
namespace UIShare.UIViewModel namespace UIShare.UIViewModel
{ {
public class ParameterVM : BindableBase public class ParameterVM : BindableBase
{ {
#region #region
@@ -45,6 +47,12 @@ namespace UIShare.UIViewModel
get => _isVisible; get => _isVisible;
set => SetProperty(ref _isVisible, value); set => SetProperty(ref _isVisible, value);
} }
private bool _isEditable = true;
public bool IsEditable
{
get => _isEditable;
set => SetProperty(ref _isEditable, value);
}
private string _name; private string _name;
public string Name public string Name

View File

@@ -8,6 +8,7 @@ namespace UIShare.UIViewModel
private string _signalName = string.Empty; private string _signalName = string.Empty;
private string _fingerprint = string.Empty; private string _fingerprint = string.Empty;
private string _methodName = string.Empty; private string _methodName = string.Empty;
private string _displayName = string.Empty;
private double _upper; private double _upper;
private double _lower; private double _lower;
private double _upperExtreme; private double _upperExtreme;
@@ -23,6 +24,14 @@ namespace UIShare.UIViewModel
get => _signalName; get => _signalName;
set => SetProperty(ref _signalName, value); set => SetProperty(ref _signalName, value);
} }
/// <summary>
/// 信号名(显示用)
/// </summary>
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
/// <summary> /// <summary>
/// 硬件指纹(物理设备唯一标识,用于超限匹配) /// 硬件指纹(物理设备唯一标识,用于超限匹配)

View File

@@ -28,7 +28,7 @@ namespace UIShare.ViewModelBase
dialogParams.Add("Message", Message); dialogParams.Add("Message", Message);
dialogParams.Add("Icon", "info"); dialogParams.Add("Icon", "info");
dialogParams.Add("ShowOk", true); dialogParams.Add("ShowOk", true);
_dialogService.ShowDialog("MessageBoxView", dialogParams, result => _dialogService.ShowDialog("MessageBox", dialogParams, result =>
{ {
callback(); callback();
}); });
@@ -41,7 +41,7 @@ namespace UIShare.ViewModelBase
dialogParams.Add("Message", Message); dialogParams.Add("Message", Message);
dialogParams.Add("Icon", "info"); dialogParams.Add("Icon", "info");
dialogParams.Add("ShowOk", true); dialogParams.Add("ShowOk", true);
_dialogService.ShowDialog("MessageBoxView", dialogParams, result => _dialogService.ShowDialog("MessageBox", dialogParams, result =>
{ {
callback(); callback();
}); });

View File

@@ -34,7 +34,7 @@ namespace UIShare.ViewModelBase
dialogParams.Add("Message", Message); dialogParams.Add("Message", Message);
dialogParams.Add("Icon", "info"); dialogParams.Add("Icon", "info");
dialogParams.Add("ShowOk", true); dialogParams.Add("ShowOk", true);
_dialogService.ShowDialog("MessageBoxView", dialogParams, result => _dialogService.ShowDialog("MessageBox", dialogParams, result =>
{ {
callback(); callback();
}); });
@@ -44,9 +44,9 @@ namespace UIShare.ViewModelBase
var dialogParams = new DialogParameters(); var dialogParams = new DialogParameters();
dialogParams.Add("Title", "错误"); dialogParams.Add("Title", "错误");
dialogParams.Add("Message", Message); dialogParams.Add("Message", Message);
dialogParams.Add("Icon", "info"); dialogParams.Add("Icon", "error");
dialogParams.Add("ShowOk", true); dialogParams.Add("ShowOk", true);
_dialogService.ShowDialog("MessageBoxView", dialogParams, result => _dialogService.ShowDialog("MessageBox", dialogParams, result =>
{ {
callback(); callback();
}); });

View File

@@ -1,4 +1,5 @@
using Common.Attributes; using Common.Attributes;
using Logger;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -24,10 +25,15 @@ namespace ZLGUSBCANFD
// 异步高性能接收线程控制 // 异步高性能接收线程控制
private volatile bool _isRunning = false; private volatile bool _isRunning = false;
private volatile bool _isClosing = false; // 关闭流程标志:通知循环发送任务尽快退出
private readonly List<Thread> _receiveThreads = new List<Thread>(); private readonly List<Thread> _receiveThreads = new List<Thread>();
// DBC 循环发送任务管理Key = (通道号, 帧ID) // DBC 循环发送任务管理Key = (通道号, 帧ID)Value = (CTS, Task)
private readonly ConcurrentDictionary<(uint , uint ID), CancellationTokenSource> _cyclicSenders = new ConcurrentDictionary<(uint, uint), CancellationTokenSource>(); private readonly ConcurrentDictionary<(uint , uint ID), (CancellationTokenSource Cts, Task SendTask)> _cyclicSenders = new ConcurrentDictionary<(uint, uint), (CancellationTokenSource, Task)>();
// 信号值持久化覆盖表Key = (通道号, 帧ID)Value = 信号名 → 物理值
// 每次 设置报文 会累积写入此表,发送时以 DBC 初始值为底再叠加此表覆盖值
private readonly ConcurrentDictionary<(uint , uint ID), Dictionary<string, double>> _signalOverrides = new ConcurrentDictionary<(uint, uint), Dictionary<string, double>>();
// 动态硬件参数 // 动态硬件参数
private readonly uint _deviceType; // 76: USBCANFD-400U private readonly uint _deviceType; // 76: USBCANFD-400U
@@ -93,15 +99,17 @@ namespace ZLGUSBCANFD
/// </summary> /// </summary>
public virtual bool (uint ) public virtual bool (uint )
{ {
_isClosing = false; // 重置关闭标志,允许循环发送
if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("请先调用 '打开设备()' 才能初始化通道。"); if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("请先调用 '打开设备()' 才能初始化通道。");
if ( >= _maxChannels) return false; if ( >= _maxChannels) return false;
lock (_channelLocks[]) lock (_channelLocks[])
{ {
// 如果已经启动过,先复位 // 如果已经启动过,直接退出
if (_channelHandles[] != IntPtr.Zero) if (_channelHandles[] != IntPtr.Zero)
{ {
ZLGCAN.ZCAN_ResetCAN(_channelHandles[]); return true;
//ZLGCAN.ZCAN_ResetCAN(_channelHandles[通道号]);
} }
// 1. 设置该通道专属的仲裁域与数据域波特率 // 1. 设置该通道专属的仲裁域与数据域波特率
@@ -145,15 +153,24 @@ namespace ZLGUSBCANFD
public virtual void CAN卡设备() public virtual void CAN卡设备()
{ {
_isRunning = false; _isClosing = true; // 通知循环发送任务尽快退出
(); _isRunning = false; // 通知接收轮询线程退出
Thread.Sleep(50); // 确保轮询线程安全退出 (); // 内部会等待所有循环发送 Task 退出
// 等待所有接收轮询线程真正退出(每个最多 1 秒)
foreach (var thread in _receiveThreads)
{
if (thread.IsAlive)
thread.Join(1000);
}
_receiveThreads.Clear(); _receiveThreads.Clear();
// 动态复位所有通道并释放DBC // 动态复位所有通道并释放 DBC(使用 TryEnter 防止死锁)
for (uint i = 0; i < _maxChannels; i++) for (uint i = 0; i < _maxChannels; i++)
{ {
lock (_channelLocks[i]) if (Monitor.TryEnter(_channelLocks[i], TimeSpan.FromSeconds(2)))
{
try
{ {
if (_channelHandles[i] != IntPtr.Zero) if (_channelHandles[i] != IntPtr.Zero)
{ {
@@ -162,6 +179,22 @@ namespace ZLGUSBCANFD
} }
DBC(i); DBC(i);
} }
finally
{
Monitor.Exit(_channelLocks[i]);
}
}
else
{
// 锁超时:循环发送 Task 可能仍阻塞在原生 API 调用中,强制清理
LoggerHelper.Info($"[ZLGCANFD] 关闭通道 {i} 时获取锁超时,强制清理");
if (_channelHandles[i] != IntPtr.Zero)
{
try { ZLGCAN.ZCAN_ResetCAN(_channelHandles[i]); } catch { }
_channelHandles[i] = IntPtr.Zero;
}
DBC(i);
}
} }
// 关闭设备主句柄 // 关闭设备主句柄
@@ -170,6 +203,8 @@ namespace ZLGUSBCANFD
ZLGCAN.ZCAN_CloseDevice(_deviceHandle); ZLGCAN.ZCAN_CloseDevice(_deviceHandle);
_deviceHandle = IntPtr.Zero; _deviceHandle = IntPtr.Zero;
} }
_isClosing = false;
} }
#endregion #endregion
@@ -349,17 +384,16 @@ namespace ZLGUSBCANFD
(, ID); (, ID);
var cts = new CancellationTokenSource(); var cts = new CancellationTokenSource();
_cyclicSenders[(, ID)] = cts; var sendTask = Task.Run(async () =>
Task.Run(async () =>
{ {
while (!cts.Token.IsCancellationRequested) while (!cts.Token.IsCancellationRequested)
{ {
if (_isClosing) break; // 关闭流程中立即退出
try try
{ {
lock (_channelLocks[]) lock (_channelLocks[])
{ {
if (_channelHandles[] == IntPtr.Zero) break; if (_channelHandles[] == IntPtr.Zero || _isClosing) break;
DBC定义报文单次(, ID, , 使CANFD); DBC定义报文单次(, ID, , 使CANFD);
} }
@@ -371,16 +405,21 @@ namespace ZLGUSBCANFD
} }
catch (Exception ex) catch (Exception ex)
{ {
// 循环发送出错时退出,避免刷屏 Logger.LoggerHelper.Error($"[ZLGCANFD] 循环发送 DBC 报文失败: {ex.Message}");
Console.WriteLine($"[ZLGCANFD] 循环发送 DBC 报文失败: {ex.Message}");
break; break;
} }
} }
_cyclicSenders.TryRemove((, ID), out var removedCts); // 清理:仅当字典中存的仍然是本任务创建的 CTS 时才移除,避免误删新任务的 CTS
removedCts?.Dispose(); if (_cyclicSenders.TryGetValue((, ID), out var current) && ReferenceEquals(current.Cts, cts))
{
_cyclicSenders.TryRemove((, ID), out _);
}
cts.Dispose();
}, cts.Token); }, cts.Token);
_cyclicSenders[(, ID)] = (cts, sendTask);
return true; return true;
} }
@@ -389,10 +428,10 @@ namespace ZLGUSBCANFD
/// </summary> /// </summary>
public virtual void (uint , uint ID) public virtual void (uint , uint ID)
{ {
if (_cyclicSenders.TryRemove((, ID), out var cts)) if (_cyclicSenders.TryRemove((, ID), out var entry))
{ {
cts.Cancel(); entry.Cts.Cancel();
cts.Dispose(); // 不在这里 Dispose由 Task 的清理代码负责释放,避免 Task 仍在使用已释放的 Token
} }
} }
@@ -401,14 +440,58 @@ namespace ZLGUSBCANFD
/// </summary> /// </summary>
public virtual void () public virtual void ()
{ {
foreach (var kvp in _cyclicSenders) var entries = _cyclicSenders.ToArray();
// 1. 取消所有 CTS不 Dispose由 Task 清理代码负责)
foreach (var kvp in entries)
{ {
kvp.Value.Cancel(); kvp.Value.Cts.Cancel();
kvp.Value.Dispose();
} }
// 2. 等待所有循环发送 Task 退出(最多 3 秒)
var tasks = new List<Task>();
foreach (var kvp in entries)
{
if (!kvp.Value.SendTask.IsCompleted)
tasks.Add(kvp.Value.SendTask);
}
if (tasks.Count > 0)
{
try
{
Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(3));
}
catch { /* 忽略等待异常 */ }
}
_cyclicSenders.Clear(); _cyclicSenders.Clear();
} }
/// <summary>
/// 清除指定通道、指定帧 ID 的信号覆盖值,恢复为 DBC 初始值。
/// </summary>
public virtual void (uint , uint ID)
{
if (_signalOverrides.TryRemove((, ID), out var dict))
{
lock (dict) dict.Clear();
}
}
/// <summary>
/// 清除指定通道所有帧的信号覆盖值。
/// </summary>
public virtual void (uint )
{
foreach (var key in _signalOverrides.Keys)
{
if (key. == && _signalOverrides.TryRemove(key, out var dict))
{
lock (dict) dict.Clear();
}
}
}
/// <summary> /// <summary>
@@ -536,7 +619,7 @@ namespace ZLGUSBCANFD
/// <param name="物理值">要写入的实际物理数值</param> /// <param name="物理值">要写入的实际物理数值</param>
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param> /// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
/// <returns>操作是否成功</returns> /// <returns>操作是否成功</returns>
public virtual bool (uint , string IDStr, string , double , int = 0) public virtual bool (uint , string IDStr, string , double , int = 0, bool = false)
{ {
if (!TryParseFrameId(IDStr, out uint ID)) if (!TryParseFrameId(IDStr, out uint ID))
{ {
@@ -548,12 +631,18 @@ namespace ZLGUSBCANFD
if (!_isDbcLoadedArray[]) return false; if (!_isDbcLoadedArray[]) return false;
if (string.IsNullOrWhiteSpace()) return false; if (string.IsNullOrWhiteSpace()) return false;
var merged = (, ID, new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase) // 将本次设置的信号值写入持久化覆盖表(累积,不会丢失之前设置的值)
var overrides = _signalOverrides.GetOrAdd((, ID), _ => new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase));
lock (overrides)
{ {
{ , } overrides[] = ;
}); }
return DBC定义报文(, ID, merged, ); // 构建发送字典DBC 初始值 + 持久化覆盖表(已包含本次设置)
var merged = (, ID);
if () return DBC定义报文(, ID, merged, );
else return true;
} }
/// <summary> /// <summary>
@@ -660,6 +749,81 @@ namespace ZLGUSBCANFD
} }
} }
/// <summary>
/// 直接发送 8 字节原始报文(经典 CAN 或 CANFD 均可)。
/// 无需加载 DBC适合手动构造简单报文。
/// </summary>
/// <param name="通道号">通道号0 ~ MaxChannels-1</param>
/// <param name="帧ID">帧 ID标准 11 位,扩展帧自动置位 0x80000000</param>
/// <param name="data">原始数据;超过 8 字节会自动截断,不足 8 字节自动补零</param>
/// <param name="是否是扩展帧">是否为 29 位扩展帧</param>
/// <param name="是否是CANFD">true = CANFD 格式false = 经典 CAN 格式</param>
/// <param name="开启波特率加速BRS">CANFD 是否开启波特率加速(经典 CAN 忽略此参数)</param>
/// <returns>是否发送成功</returns>
public virtual bool (
uint ,
uint ID,
byte[] data,
bool = false,
bool CANFD = true,
bool BRS = true)
{
if ( >= _maxChannels || _channelHandles[] == IntPtr.Zero) return false;
byte[] buffer = new byte[8];
if (data != null)
{
Array.Copy(data, 0, buffer, 0, Math.Min(data.Length, 8));
}
uint canId = ID & 0x7FFFFFFF;
if () canId |= 0x80000000;
lock (_channelLocks[])
{
if (CANFD)
{
ZLGCAN.canfd_frame fdFrame = new ZLGCAN.canfd_frame
{
can_id = canId,
len = 8,
flags = (byte)(( ? 0x01 : 0x00) | (BRS ? 0x02 : 0x00) | 0x20),
data = new byte[64]
};
Array.Copy(buffer, 0, fdFrame.data, 0, 8);
ZLGCAN.ZCAN_TransmitFD_Data txFdData = new ZLGCAN.ZCAN_TransmitFD_Data { frame = fdFrame, transmit_type = 2 };
IntPtr pTxFd = Marshal.AllocHGlobal(Marshal.SizeOf(txFdData));
try
{
Marshal.StructureToPtr(txFdData, pTxFd, true);
return ZLGCAN.ZCAN_TransmitFD(_channelHandles[], pTxFd, 1) == 1;
}
finally { Marshal.FreeHGlobal(pTxFd); }
}
else
{
ZLGCAN.can_frame standardFrame = new ZLGCAN.can_frame
{
can_id = canId,
can_dlc = 8,
__pad = 0x20,
data = new byte[8]
};
Array.Copy(buffer, 0, standardFrame.data, 0, 8);
ZLGCAN.ZCAN_Transmit_Data txData = new ZLGCAN.ZCAN_Transmit_Data { frame = standardFrame, transmit_type = 2 };
IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(txData));
try
{
Marshal.StructureToPtr(txData, pTx, true);
return ZLGCAN.ZCAN_Transmit(_channelHandles[], pTx, 1) == 1;
}
finally { Marshal.FreeHGlobal(pTx); }
}
}
}
/// <summary> /// <summary>
/// 构建发送用信号字典DBC 初始值 + 额外覆盖。 /// 构建发送用信号字典DBC 初始值 + 额外覆盖。
/// 调用方需自行保证通道已初始化且 DBC 已加载。 /// 调用方需自行保证通道已初始化且 DBC 已加载。
@@ -678,6 +842,7 @@ namespace ZLGUSBCANFD
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage)); var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
var dict = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
// 1. DBC 初始值打底
for (int i = 0; i < msg.nSignalCount; i++) for (int i = 0; i < msg.nSignalCount; i++)
{ {
var signal = msg.vSignals[i]; var signal = msg.vSignals[i];
@@ -688,6 +853,17 @@ namespace ZLGUSBCANFD
} }
} }
// 2. 叠加持久化覆盖表(之前 设置报文 累积的值)
if (_signalOverrides.TryGetValue((, ID), out var persisted))
{
lock (persisted)
{
foreach (var kvp in persisted)
dict[kvp.Key] = kvp.Value;
}
}
// 3. 叠加本次调用传入的额外覆盖(优先级最高)
if ( != null) if ( != null)
{ {
foreach (var kvp in ) foreach (var kvp in )
@@ -701,6 +877,7 @@ namespace ZLGUSBCANFD
} }
finally finally
{ {
Marshal.FreeHGlobal(ptrDbcMsg); Marshal.FreeHGlobal(ptrDbcMsg);
} }
} }

View File

@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Logger\Logger.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

BIN
加密狗/ViKey.dll Normal file

Binary file not shown.

BIN
加密狗/ViKey7.4.4.dll Normal file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- 配置 ViKey.dll 复制到输出目录 -->
<None Include="ViKey.dll">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace
{
public enum VikeyType
{
ViKeyAPP = 0, //实用型加密狗
ViKeySTD = 1, //标准型加密狗
ViKeyNET = 2, //网络型加密狗
ViKeyPRO = 3, //专业型加密狗
ViKeyWEB = 4, //身份认证型加密狗
ViKeyTIME = 5, //时钟型加密狗,内部独立时钟
ViKeyMultiFunctional = 0x0A, //多功能加密狗 支持软件加密 支持文档加密
ViKeyMultiFunctionalTime = 0x0B, //多功能时钟加密狗
ViKeyInvalid //无效类型
};
public enum : uint
{
/// <summary>
/// 成功
/// </summary>
VIKEY_SUCCESS = 0x00000000,
/// <summary>
/// 没有找到ViKey加密锁
/// </summary>
VIKEY_ERROR_NO_VIKEY = 0x80000001,
/// <summary>
/// 密码错误
/// </summary>
VIKEY_ERROR_INVALID_PASSWORD = 0x80000002,
/// <summary>
/// 请先查找加密锁
/// </summary>
VIKEY_ERROR_NEED_FIND = 0x80000003,
/// <summary>
/// 无效的句柄
/// </summary>
VIKEY_ERROR_INVALID_INDEX = 0x80000004,
/// <summary>
/// 数值错误
/// </summary>
VIKEY_ERROR_INVALID_VALUE = 0x80000005,
/// <summary>
/// 秘钥无效
/// </summary>
VIKEY_ERROR_INVALID_KEY = 0x80000006,
/// <summary>
/// 读取信息错误
/// </summary>
VIKEY_ERROR_GET_VALUE = 0x80000007,
/// <summary>
/// 设置信息错误
/// </summary>
VIKEY_ERROR_SET_VALUE = 0x80000008,
/// <summary>
/// 没有机会
/// </summary>
VIKEY_ERROR_NO_CHANCE = 0x80000009,
/// <summary>
/// 权限不足
/// </summary>
VIKEY_ERROR_NO_TAUTHORITY = 0x8000000A,
/// <summary>
/// 地址或长度错误
/// </summary>
VIKEY_ERROR_INVALID_ADDR_OR_SIZE = 0x8000000B,
/// <summary>
/// 获取随机数错误
/// </summary>
VIKEY_ERROR_RANDOM = 0x8000000C,
/// <summary>
/// 获取种子错误
/// </summary>
VIKEY_ERROR_SEED = 0x8000000D,
/// <summary>
/// 通信错误
/// </summary>
VIKEY_ERROR_CONNECTION = 0x8000000E,
/// <summary>
/// 算法或计算错误
/// </summary>
VIKEY_ERROR_CALCULATE = 0x8000000F,
/// <summary>
/// 计数器错误
/// </summary>
VIKEY_ERROR_MODULE = 0x80000010,
/// <summary>
/// 产生密码错误
/// </summary>
VIKEY_ERROR_GENERATE_NEW_PASSWORD = 0x80000011,
/// <summary>
/// 加密数据错误
/// </summary>
VIKEY_ERROR_ENCRYPT_FAILED = 0x80000012,
/// <summary>
/// 解密数据错误
/// </summary>
VIKEY_ERROR_DECRYPT_FAILED = 0x80000013,
/// <summary>
/// ViKey加密锁已经被锁定
/// </summary>
VIKEY_ERROR_ALREADY_LOCKED = 0x80000014,
/// <summary>
/// 无效的命令
/// </summary>
VIKEY_ERROR_UNKNOWN_COMMAND = 0x80000015,
/// <summary>
/// 当前ViKey加密锁不支持此功能
/// </summary>
VIKEY_ERROR_NO_SUPPORT = 0x80000016,
/// <summary>
/// 发生异常
/// </summary>
VIKEY_ERROR_CATCH = 0x80000017,
/// <summary>
/// 未知错误
/// </summary>
VIKEY_ERROR_UNKNOWN_ERROR = 0xFFFFFFFF,
}
public class API类
{
/// <summary>
/// 查找加密锁。使用其他API前必须先调用次函数。
/// </summary>
/// <param name="pdwCount">[out] 如果查找到系统中存在加密狗,返回查找到加密狗的个数</param>
/// <returns>0表示系统中存在ViKey加密狗。</returns>
[DllImport("ViKey.dll")]
public static extern uint VikeyFind(ref uint pdwCount);
/// <summary>
/// 获取加密狗的硬件ID,加密狗的硬件ID是加密狗的唯一标识每个加密狗的硬件ID都不一样。
/// </summary>
/// <param name="Index">[in]指定要操作加密狗的序号</param>
/// <param name="pdwHID">[out]返回加密狗的硬件ID</param>
/// <returns></returns>
[DllImport("ViKey.dll")]
public static extern uint VikeyGetHID(ushort Index, out uint pdwHID);
/// <summary>
/// 获取加密狗的当前权限
/// </summary>
/// <param name="Index">指定要操作加密狗的序号</param>
/// <param name="pLevel">返回加密狗的当前权限 0表示加密狗尚未登录 1表示加密狗是用户权限 2表示加密狗是管理员权限</param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyGetLevel(ushort Index, out byte pLevel);
/// <summary>
/// 以用户权限登录加密狗
/// </summary>
/// <param name="Index"></param>
/// <param name="pUserPassushort"></param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern uint VikeyUserLogin(ushort Index, Byte[] pUserPassword);
/// <summary>
/// 以管理员权限登录加密狗
/// </summary>
/// <param name="Index"></param>
/// <param name="pAdminPassword"></param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern uint VikeyAdminLogin(ushort Index, Byte[] pAdminPassword);
/// <summary>
/// 注销登录加密狗 注销后的加密狗权限为0
/// </summary>
/// <param name="Index">[in]指定要操作加密狗的序号</param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern uint VikeyLogoff(ushort Index);
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SVikeyTime
{
public byte cYear;
public byte cMonth;
public byte cDay;
public byte cHour;
public byte cMinute;
public byte cSecond;
}
/// <summary>
/// 获取时钟型加密狗中的内部独立时间
/// </summary>
/// <param name="Index"></param>
/// <param name="pTime"></param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyGetTime(ushort Index, out SVikeyTime pTime);
/// <summary>
/// 获取时钟型加密狗中的到期时间
/// </summary>
/// <param name="Index"></param>
/// <param name="pTime"></param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyGetValidTime(ushort Index, out SVikeyTime pTime);
/// <summary>
/// 检测时钟型加密狗的时钟功能是否到期
/// </summary>
/// <param name="Index">[in]指定要操作加密狗的序号</param>
/// <param name="pIsValid">[out]返回是否到期结果 1表示没有到期 0表示已经到期</param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyCheckValidTime(ushort Index, out byte pIsValid);
/// <summary>
/// 从加密狗中的获取4个双字的随机数数据
/// </summary>
/// <param name="Index">指定要操作加密狗的序号</param>
/// <param name="pwRandom1"></param>
/// <param name="pwRandom2"></param>
/// <param name="pwRandom3"></param>
/// <param name="pwRandom4"></param>
/// <returns></returns>
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint ViKeyRandom(ushort Index,
out ushort pwRandom1, out ushort pwRandom2, out ushort pwRandom3, out ushort pwRandom4);
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyReadData(ushort Index, ushort Addr, ushort Length, byte[] buffer);
[DllImport("ViKey.dll", CallingConvention = CallingConvention.StdCall)]
public static extern uint VikeyWriteData(ushort Index, ushort Addr, ushort Length, byte[] buffer);
/// <summary>
/// // 7.4.6版本(含)以后的加密狗必须在查找加密狗前调用VikeySetUserSN并输入正确的UserSN后面才能查找到所属用户的加密狗
// 7.4.6版本以前的加密狗可以忽略此函数 不用调用此函数
// UserSN在ViKey加密管理工具的->帮助中心页面->复制出来
/// </summary>
/// <param name="pUserSN"></param>
/// <returns></returns>
[DllImport("ViKey")]
public static extern uint VikeySetUserSN(Byte[] pUserSN);
/// <summary>
/// //设置加密狗ApiKey 硬件版本为7.4.5(含)以后的加密狗必须要调用此接口才能登录
//ApiKey 是由ViKey加密管理工具的->加密狗管理->密钥设置页面 通过Api密钥种子生成的
/// </summary>
/// <param name="Index"></param>
/// <param name="pApiKey"></param>
/// <returns></returns>
[DllImport("ViKey")]
public static extern uint VikeySetApiKey(ushort Index, Byte[] pApiKey);
[DllImport("ViKey")]
public static extern uint VikeyUninitialization();
}
}

View File

@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace
{
public class
{
public static void Uninitialization()
{
API类.VikeyUninitialization();
}
// 保留原有方法,但添加新方法
public static int (ushort Index, string , string )
{
// 保持不变
uint = 0;
var re = API类.VikeyFind(ref );
if (re != 0) return -1;
API类.VikeyAdminLogin(Index, Encoding.Default.GetBytes());
SHA256 sHA256 = SHA256.Create();
API类.VikeyGetHID(Index, out uint pid);
byte[] HID = BitConverter.GetBytes(pid);
var pswd = Encoding.UTF8.GetBytes();
byte[] = HID.Concat(pswd).ToArray();
var = sHA256.ComputeHash();
API类.VikeyWriteData(Index, 0, 32, );
byte[] randomBytes = new byte[96];
Random random = new Random();
random.NextBytes(randomBytes);
API类.VikeyWriteData(Index, 32, 96, randomBytes);
return 0;
}
/// <summary>
/// 新方法:仅检查加密狗时间,不验证密码和数据
/// </summary>
public static int ()
{
uint = 0;
var re = API类.VikeyFind(ref );
if (re != 0 || == 0) return -1;
// 直接返回第一个找到的加密狗
return 0;
}
/// <summary>
/// 原有方法:寻找加密狗并进行完整验证
/// </summary>
public static int (string ,string )
{
uint = 0;
var re = API类.VikeyFind(ref );
if (re != 0) return -1;
int = -1;
for (ushort i = 0; i < ; i++)
{
re = API类.VikeyUserLogin(i, Encoding.Default.GetBytes());
if ((i, )) return i;
}
return (int);
}
public static bool (ushort Index, string )
{
// 保持不变
SHA256 sHA256 = SHA256.Create();
API类.VikeyGetHID(Index, out uint pid);
byte[] HID = BitConverter.GetBytes(pid);
var pswd = Encoding.UTF8.GetBytes();
byte[] = HID.Concat(pswd).ToArray();
var = sHA256.ComputeHash();
var buffer = new byte[32];
API类.VikeyReadData(Index, 0, 32, buffer);
return buffer.SequenceEqual();
}
public static TimeSpan (int )
{
ushort = (ushort);
try
{
// 读时间前必须先登录(用户权限即可)
uint re = API类.VikeyUserLogin(, Encoding.Default.GetBytes("11111111"));
if (re != 0)
{
Console.WriteLine($"VikeyUserLogin 失败: 0x{re:X}");
return TimeSpan.Zero;
}
re = API类.VikeyGetTime(, out API类.SVikeyTime );
if (re != 0)
{
Console.WriteLine($"VikeyGetTime 失败: 0x{re:X}");
return TimeSpan.Zero;
}
re = API类.VikeyGetValidTime(, out API类.SVikeyTime );
if (re != 0)
{
Console.WriteLine($"VikeyGetValidTime 失败: 0x{re:X}");
return TimeSpan.Zero;
}
re = API类.VikeyCheckValidTime(, out byte isValid);
Console.WriteLine($"VikeyCheckValidTime: ret=0x{re:X}, isValid={isValid}");
// cYear 是 byteViKey 用距 2000 年的偏移量
DateTime _现在时间 = new DateTime(.cYear + 2000, .cMonth, .cDay,
.cHour, .cMinute, .cSecond);
DateTime _到期时间 = new DateTime(.cYear + 2000, .cMonth, .cDay,
.cHour, .cMinute, .cSecond);
Console.WriteLine($"加密狗当前时间: {_现在时间:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"加密狗到期时间: {_到期时间:yyyy-MM-dd HH:mm:ss}");
return _到期时间 - _现在时间;
}
catch (Exception ex)
{
Console.WriteLine($"检查剩余时间异常: {ex.Message}");
return TimeSpan.Zero;
}
}
public static uint SN(Byte[] buffer)
{
return API类.VikeySetUserSN(buffer);
}
public static uint APIkey(ushort index, Byte[] buffer)
{
return API类.VikeySetApiKey(index, buffer);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using ;
// ═══════════════════════════════════════════════════════
// 刷入脚本(独立运行,不属于 ADP 主程序流程)
// 使用前请先用 ViKey 管理工具设置好密码和到期时间
// ═══════════════════════════════════════════════════════
public class Program
{
public static void Main(string[] args)
{
//写入加密狗(需管理员密码)
.(0, "00000000", "ADP项目");
}
}