diff --git a/ACP.sln b/ACP.sln
new file mode 100644
index 0000000..eb1924a
--- /dev/null
+++ b/ACP.sln
@@ -0,0 +1,163 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35527.113
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{520D7017-1A2F-427D-B8A4-54E3D0575EFD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{42623437-0ADD-4149-B33F-DB30E7B13D69}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{F9A522A1-869E-462E-9782-57ED3DE5566E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ORM", "ORM\ORM.csproj", "{19ED286B-4E83-4EA0-9BD4-37D2AB796F22}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Logger\Logger.csproj", "{84F26384-D1DC-4175-98A3-A64A4FB961D2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{1F4E6A3D-B273-4F76-8D59-37F7C8E858BB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Command", "Command\Command.csproj", "{FC353140-A722-4E8E-97BA-FB866E8188A9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIShare", "UIShare\UIShare.csproj", "{53198B54-BDF9-446D-B1FD-EC0A52D794E5}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{1E92D601-68FC-45F6-8251-BD0F39226E5B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SettingModule", "SettingModule\SettingModule.csproj", "{51384EB4-1940-44FB-8E18-56F7201642CD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingModule", "TestingModule\TestingModule.csproj", "{BD74E7F3-BBB5-4375-A37E-F24E8507A436}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\MainModule.csproj", "{DFC084AE-FE45-4492-A2DA-EAE2941F26E9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoMoudle", "UpdateInfoMoudle\UpdateInfoMoudle.csproj", "{3456C3AC-E7FF-45F8-A68F-BEED9969812C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonitorModule", "MonitorModule\MonitorModule.csproj", "{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceEditModule", "DeviceEditModule\DeviceEditModule.csproj", "{170AD4C1-189D-4FBE-B10D-2A4304527834}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZLGUSBCANFD", "ZLGUSBCANFD\ZLGUSBCANFD.csproj", "{542FE380-2344-4343-9AC6-F0C587C13B88}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchMovementModule", "BenchMovementModule\BenchMovementModule.csproj", "{31F84D0E-3634-4C70-A830-21AF412E3BBF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CANModule", "CANModule\CANModule.csproj", "{AF2533DD-599D-49D7-942B-B0C26534D15F}"
+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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACP", "ACP\ACP.csproj", "{CC810066-06A3-4635-96C7-1D30D78C2130}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {520D7017-1A2F-427D-B8A4-54E3D0575EFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {520D7017-1A2F-427D-B8A4-54E3D0575EFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {520D7017-1A2F-427D-B8A4-54E3D0575EFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {520D7017-1A2F-427D-B8A4-54E3D0575EFD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {42623437-0ADD-4149-B33F-DB30E7B13D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42623437-0ADD-4149-B33F-DB30E7B13D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42623437-0ADD-4149-B33F-DB30E7B13D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42623437-0ADD-4149-B33F-DB30E7B13D69}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F9A522A1-869E-462E-9782-57ED3DE5566E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F9A522A1-869E-462E-9782-57ED3DE5566E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F9A522A1-869E-462E-9782-57ED3DE5566E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F9A522A1-869E-462E-9782-57ED3DE5566E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19ED286B-4E83-4EA0-9BD4-37D2AB796F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19ED286B-4E83-4EA0-9BD4-37D2AB796F22}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19ED286B-4E83-4EA0-9BD4-37D2AB796F22}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19ED286B-4E83-4EA0-9BD4-37D2AB796F22}.Release|Any CPU.Build.0 = Release|Any CPU
+ {84F26384-D1DC-4175-98A3-A64A4FB961D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {84F26384-D1DC-4175-98A3-A64A4FB961D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {84F26384-D1DC-4175-98A3-A64A4FB961D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {84F26384-D1DC-4175-98A3-A64A4FB961D2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F4E6A3D-B273-4F76-8D59-37F7C8E858BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F4E6A3D-B273-4F76-8D59-37F7C8E858BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F4E6A3D-B273-4F76-8D59-37F7C8E858BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F4E6A3D-B273-4F76-8D59-37F7C8E858BB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC353140-A722-4E8E-97BA-FB866E8188A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC353140-A722-4E8E-97BA-FB866E8188A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC353140-A722-4E8E-97BA-FB866E8188A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC353140-A722-4E8E-97BA-FB866E8188A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {53198B54-BDF9-446D-B1FD-EC0A52D794E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {53198B54-BDF9-446D-B1FD-EC0A52D794E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {53198B54-BDF9-446D-B1FD-EC0A52D794E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {53198B54-BDF9-446D-B1FD-EC0A52D794E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72}.Release|Any CPU.Build.0 = Release|Any CPU
+ {51384EB4-1940-44FB-8E18-56F7201642CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {51384EB4-1940-44FB-8E18-56F7201642CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {51384EB4-1940-44FB-8E18-56F7201642CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {51384EB4-1940-44FB-8E18-56F7201642CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD74E7F3-BBB5-4375-A37E-F24E8507A436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD74E7F3-BBB5-4375-A37E-F24E8507A436}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD74E7F3-BBB5-4375-A37E-F24E8507A436}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD74E7F3-BBB5-4375-A37E-F24E8507A436}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DFC084AE-FE45-4492-A2DA-EAE2941F26E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DFC084AE-FE45-4492-A2DA-EAE2941F26E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DFC084AE-FE45-4492-A2DA-EAE2941F26E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DFC084AE-FE45-4492-A2DA-EAE2941F26E9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3456C3AC-E7FF-45F8-A68F-BEED9969812C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3456C3AC-E7FF-45F8-A68F-BEED9969812C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3456C3AC-E7FF-45F8-A68F-BEED9969812C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3456C3AC-E7FF-45F8-A68F-BEED9969812C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Release|Any CPU.Build.0 = Release|Any CPU
+ {170AD4C1-189D-4FBE-B10D-2A4304527834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {170AD4C1-189D-4FBE-B10D-2A4304527834}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {170AD4C1-189D-4FBE-B10D-2A4304527834}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {170AD4C1-189D-4FBE-B10D-2A4304527834}.Release|Any CPU.Build.0 = Release|Any CPU
+ {542FE380-2344-4343-9AC6-F0C587C13B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {542FE380-2344-4343-9AC6-F0C587C13B88}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {542FE380-2344-4343-9AC6-F0C587C13B88}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {542FE380-2344-4343-9AC6-F0C587C13B88}.Release|Any CPU.Build.0 = Release|Any CPU
+ {31F84D0E-3634-4C70-A830-21AF412E3BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {31F84D0E-3634-4C70-A830-21AF412E3BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {31F84D0E-3634-4C70-A830-21AF412E3BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {31F84D0E-3634-4C70-A830-21AF412E3BBF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF2533DD-599D-49D7-942B-B0C26534D15F}.Debug|Any CPU.ActiveCfg = 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.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
+ {CC810066-06A3-4635-96C7-1D30D78C2130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC810066-06A3-4635-96C7-1D30D78C2130}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC810066-06A3-4635-96C7-1D30D78C2130}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC810066-06A3-4635-96C7-1D30D78C2130}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {DF1DE1F7-B570-41D7-A2B5-AB2CDCD4BD72} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {51384EB4-1940-44FB-8E18-56F7201642CD} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {BD74E7F3-BBB5-4375-A37E-F24E8507A436} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {DFC084AE-FE45-4492-A2DA-EAE2941F26E9} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {3456C3AC-E7FF-45F8-A68F-BEED9969812C} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
+ {850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {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}
+ {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
+EndGlobal
diff --git a/ACP/ACP.csproj b/ACP/ACP.csproj
new file mode 100644
index 0000000..d830b54
--- /dev/null
+++ b/ACP/ACP.csproj
@@ -0,0 +1,53 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/ACP/App.xaml b/ACP/App.xaml
new file mode 100644
index 0000000..8c23c08
--- /dev/null
+++ b/ACP/App.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ACP/App.xaml.cs b/ACP/App.xaml.cs
new file mode 100644
index 0000000..fa86616
--- /dev/null
+++ b/ACP/App.xaml.cs
@@ -0,0 +1,163 @@
+using Common;
+using ACP.ViewModels;
+using ACP.ViewModels.Dialogs;
+using ACP.Views;
+using ACP.Views;
+using ACP.Views.Dialogs;
+using Logger;
+using Notifications.Wpf.Core;
+using ORM;
+using Service.Implement;
+using Service.Interface;
+using System.Configuration;
+using System.Data;
+using System.Reflection;
+using System.Windows;
+using UIShare.PubEvent;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+using UIShare.GlobalVariable;
+using UIShare;
+using System;
+using DeviceCommand.Devices;
+using DeviceCommand.Base;
+using AutoMapper;
+using Microsoft.Extensions.Logging.Abstractions;
+using ACP.Profiles;
+using 加密狗;
+
+namespace ACP
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : PrismApplication
+ {
+ protected override Window CreateShell()
+ {
+ //UI线程未捕获异常处理事件
+ this.DispatcherUnhandledException += OnDispatcherUnhandledException;
+ //Task线程内未捕获异常处理事件
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
+ ////多线程异常
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ return Container.Resolve();
+ }
+ private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
+ {
+ LoggerHelper.Error(e.Exception.Message, e.Exception.StackTrace);
+ }
+
+ private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
+ {
+ LoggerHelper.Error(e.Exception.Message, e.Exception.StackTrace);
+ }
+
+ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ //记录dump文件
+ Exception ex = e.ExceptionObject as Exception;
+ MiniDump.TryDump($"dumps\\Error_{DateTime.Now:yyyy-MM-dd HH-mm-ss-ms}.dmp", MiniDump.Option.WithFullMemory, ex);
+ }
+ protected override void OnInitialized()
+ {
+ // 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea
+ var globalInfo = Container.Resolve();
+ LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
+
+ //初始化数据库
+ DatabaseConfig.SetTenant(10001);
+ DatabaseConfig.InitSqlite();
+ DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
+ SqlSugarContext.InitDatabase();
+ //显示登录窗口
+ var login = Container.Resolve();
+ var re = Container.Resolve();
+ RegionManager.SetRegionManager(login, re);
+ RegionManager.SetRegionManager(Application.Current.MainWindow, re);
+ 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)
+ {
+ base.RegisterRequiredTypes(containerRegistry);
+ //注册全局变量
+ containerRegistry.RegisterScoped();
+ containerRegistry.RegisterScoped();
+ containerRegistry.RegisterScoped();
+ containerRegistry.RegisterScoped();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ //注册AutoMapper
+ var config = new MapperConfiguration(
+ cfg => cfg.AddProfile(),
+ NullLoggerFactory.Instance
+ );
+ containerRegistry.RegisterSingleton(() => config.CreateMapper());
+ }
+ protected override void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ //注册弹窗
+ containerRegistry.RegisterDialog("MessageBox");
+ // 注册通知管理器
+ INotificationManager NotificationManager = new NotificationManager();
+ containerRegistry.RegisterInstance(NotificationManager);
+ // 注册仓储
+ containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
+ //注册服务
+ containerRegistry.Register();
+ containerRegistry.Register();
+ }
+ //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
+ protected override IModuleCatalog CreateModuleCatalog()
+ {
+ //指定模块加载方式为从文件夹中以反射发现并加载module(推荐用法)
+ return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
+ }
+ }
+}
diff --git a/ACP/AssemblyInfo.cs b/ACP/AssemblyInfo.cs
new file mode 100644
index 0000000..b0ec827
--- /dev/null
+++ b/ACP/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/ACP/Images/error.png b/ACP/Images/error.png
new file mode 100644
index 0000000..de079d0
Binary files /dev/null and b/ACP/Images/error.png differ
diff --git a/ACP/Images/info.png b/ACP/Images/info.png
new file mode 100644
index 0000000..8fed1fb
Binary files /dev/null and b/ACP/Images/info.png differ
diff --git a/ACP/Images/warning.png b/ACP/Images/warning.png
new file mode 100644
index 0000000..1acd082
Binary files /dev/null and b/ACP/Images/warning.png differ
diff --git a/ACP/Profiles/AutoMapperProfile.cs b/ACP/Profiles/AutoMapperProfile.cs
new file mode 100644
index 0000000..5d9f5de
--- /dev/null
+++ b/ACP/Profiles/AutoMapperProfile.cs
@@ -0,0 +1,64 @@
+using AutoMapper;
+using Model.Models;
+using System;
+using UIShare.UIViewModel;
+
+namespace ACP.Profiles
+{
+ ///
+ /// UIShare.UIViewModel ↔ Model.Models 双向映射配置。
+ ///
+ public class AutoMapperProfile : Profile
+ {
+ public AutoMapperProfile()
+ {
+ // ===== Parameter =====
+ // ParameterModel.Type(System.Type) ↔ Parameter.TypeName(string)
+ // ParameterModel.Category(enum) ↔ Parameter.Category(string)
+ CreateMap()
+ .ForMember(dest => dest.TypeName,
+ opt => opt.MapFrom(src => src.Type != null ? src.Type.AssemblyQualifiedName : null))
+ .ForMember(dest => dest.Category,
+ opt => opt.MapFrom(src => src.Category.ToString()));
+
+ CreateMap()
+ .ForMember(dest => dest.Type,
+ opt => opt.MapFrom(src => string.IsNullOrEmpty(src.TypeName)
+ ? typeof(string)
+ : (Type.GetType(src.TypeName) ?? typeof(string))))
+ .ForMember(dest => dest.Category,
+ opt => opt.MapFrom(src => ParseCategory(src.Category)));
+
+ // ===== Step =====
+ CreateMap().ReverseMap();
+
+ // ===== Program =====
+ CreateMap().ReverseMap();
+
+ // ===== Method =====
+ CreateMap().ReverseMap();
+
+ // ===== DeviceInfo =====
+ CreateMap().ReverseMap();
+
+ // ===== CanMessageShow =====
+ CreateMap().ReverseMap();
+
+
+ // ===== InstructionNode =====
+ CreateMap().ReverseMap();
+
+ // ===== SubProgramItem =====
+ CreateMap().ReverseMap();
+ }
+
+ private static ParameterVM.ParameterCategory ParseCategory(string? category)
+ {
+ if (string.IsNullOrEmpty(category))
+ return ParameterVM.ParameterCategory.Temp;
+ return Enum.TryParse(category, true, out var result)
+ ? result
+ : ParameterVM.ParameterCategory.Temp;
+ }
+ }
+}
diff --git a/ACP/Resources/Images/error.png b/ACP/Resources/Images/error.png
new file mode 100644
index 0000000..de079d0
Binary files /dev/null and b/ACP/Resources/Images/error.png differ
diff --git a/ACP/Resources/Images/info.png b/ACP/Resources/Images/info.png
new file mode 100644
index 0000000..8fed1fb
Binary files /dev/null and b/ACP/Resources/Images/info.png differ
diff --git a/ACP/Resources/Images/warning.png b/ACP/Resources/Images/warning.png
new file mode 100644
index 0000000..1acd082
Binary files /dev/null and b/ACP/Resources/Images/warning.png differ
diff --git a/ACP/ViewModels/Dialogs/MessageBoxViewModel.cs b/ACP/ViewModels/Dialogs/MessageBoxViewModel.cs
new file mode 100644
index 0000000..19ce3a6
--- /dev/null
+++ b/ACP/ViewModels/Dialogs/MessageBoxViewModel.cs
@@ -0,0 +1,123 @@
+using UIShare.PubEvent;
+using UIShare.ViewModelBase;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace ACP.ViewModels.Dialogs
+{
+ public class MessageBoxViewModel : DialogViewModelBase
+ {
+ #region 属性
+
+ private string _Title;
+ public string Title
+ {
+ get => _Title;
+ set => SetProperty(ref _Title, value);
+ }
+
+ private string _Message = "";
+ public string Message
+ {
+ get => _Message;
+ set => SetProperty(ref _Message, value);
+ }
+
+ private ImageSource _Icon = new System.Windows.Media.Imaging.BitmapImage(new System.Uri("pack://application:,,,/Images/info.png"));
+ public ImageSource Icon
+ {
+ get => _Icon;
+ set => SetProperty(ref _Icon, value);
+ }
+
+ private bool _ShowYes;
+ public bool ShowYes
+ {
+ get => _ShowYes;
+ set => SetProperty(ref _ShowYes, value);
+ }
+
+ private bool _ShowNo;
+ public bool ShowNo
+ {
+ get => _ShowNo;
+ set => SetProperty(ref _ShowNo, value);
+ }
+
+ private bool _ShowOk;
+ public bool ShowOk
+ {
+ get => _ShowOk;
+ set => SetProperty(ref _ShowOk, value);
+ }
+
+ private bool _ShowCancel;
+ public bool ShowCancel
+ {
+ get => _ShowCancel;
+ set => SetProperty(ref _ShowCancel, value);
+ }
+
+ #endregion
+
+ #region 命令
+ public ICommand YesCommand { get; set; }
+ public ICommand NoCommand { get; set; }
+ public ICommand OkCommand { get; set; }
+ public ICommand CancelCommand { get; set; }
+ #endregion
+
+ public DialogCloseListener RequestClose { get; set; }
+
+ public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
+ {
+ YesCommand = new DelegateCommand(OnYes);
+ NoCommand = new DelegateCommand(OnNo);
+ OkCommand = new DelegateCommand(OnOk);
+ CancelCommand = new DelegateCommand(OnCancel);
+ }
+
+ private void CloseDialog(ButtonResult result)
+ {
+ var parameters = new DialogParameters();
+ RequestClose.Invoke(new DialogResult(result));
+ }
+
+ private void OnYes() => CloseDialog(ButtonResult.Yes);
+ private void OnNo() => CloseDialog(ButtonResult.No);
+ private void OnOk() => CloseDialog(ButtonResult.OK);
+ private void OnCancel() => CloseDialog(ButtonResult.Cancel);
+
+ #region Prism Dialog 规范
+ public bool CanCloseDialog() => true;
+
+ public override void OnDialogClosed()
+ {
+ _eventAggregator.GetEvent().Publish(false);
+ }
+
+ public override void OnDialogOpened(IDialogParameters parameters)
+ {
+ _eventAggregator.GetEvent().Publish(true);
+ Title = parameters.GetValue("Title");
+ Message = parameters.GetValue("Message");
+ var iconKey = parameters.GetValue("Icon"); // info / error / warn
+ var bmp = (ImageSource)new System.Windows.Media.Imaging.BitmapImage(new System.Uri(iconKey switch
+ {
+ "info" => "pack://application:,,,/Images/info.png",
+ "error" => "pack://application:,,,/Images/error.png",
+ "warn" => "pack://application:,,,/Images/warning.png",
+ _ => "pack://application:,,,/Images/info.png"
+ }));
+ bmp.Freeze();
+ Icon = bmp;
+
+
+ ShowYes = parameters.GetValue("ShowYes");
+ ShowNo = parameters.GetValue("ShowNo");
+ ShowOk = parameters.GetValue("ShowOk");
+ ShowCancel = parameters.GetValue("ShowCancel");
+ }
+ #endregion
+ }
+}
diff --git a/ACP/ViewModels/ShellViewModel.cs b/ACP/ViewModels/ShellViewModel.cs
new file mode 100644
index 0000000..7929ab4
--- /dev/null
+++ b/ACP/ViewModels/ShellViewModel.cs
@@ -0,0 +1,884 @@
+
+using DeviceCommand.Devices;
+using Logger;
+using MaterialDesignThemes.Wpf;
+using Microsoft.Win32;
+using Model.Models;
+using Notifications.Wpf.Core;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Threading;
+using UIShare;
+using UIShare.GlobalVariable;
+using UIShare.PubEvent;
+using UIShare.UIViewModel;
+using SqlSugar;
+
+namespace ACP.ViewModels
+{
+ public class ShellViewModel : BindableBase
+ {
+ #region 私有字段
+ private string _Title = "";
+ private bool _IsLeftDrawerOpen;
+
+ private readonly ConcurrentDictionary _executionTasks = new();
+ private readonly ConcurrentDictionary _errorExecutionTasks = new();
+
+ private readonly IEventAggregator _eventAggregator;
+ private readonly IRegionManager _regionManager;
+ private readonly IDialogService _dialogService;
+ private readonly IContainerProvider _containerProvider;
+ private readonly INotificationManager _notificationManager;
+ private readonly IModuleManager _moduleManager;
+ private readonly GlobalInfo _globalInfo;
+
+ // 💡 新增:UI 刷新定时器,用于高频让前端重新拉取当前工位的 SW 累计时间
+ private readonly DispatcherTimer _uiRefreshTimer;
+ #endregion
+
+ #region 属性
+ public string Title
+ {
+ get => _Title;
+ set => SetProperty(ref _Title, value);
+ }
+ public bool IsLeftDrawerOpen
+ {
+ get => _IsLeftDrawerOpen;
+ set => SetProperty(ref _IsLeftDrawerOpen, value);
+ }
+
+ public TimeSpan RunningTime
+ {
+ get
+ {
+ if (CurrentContext == null) return TimeSpan.Zero;
+ return CurrentContext.SW.IsRunning ? CurrentContext.SW.Elapsed : CurrentContext.RunningTime;
+ }
+ set
+ {
+ if (CurrentContext != null && CurrentContext.RunningTime != value)
+ {
+ CurrentContext.RunningTime = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ public bool IsTerminate
+ {
+ get => CurrentContext?.IsTerminate ?? false;
+ set
+ {
+ if (CurrentContext != null && CurrentContext.IsTerminate != value)
+ {
+ CurrentContext.IsTerminate = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private ScopedContext? CurrentContext =>
+ _globalInfo.ContextDic.TryGetValue(_globalInfo.CurrentScope, out var ctx) ? ctx : null;
+ private StepRunning? CurrentRunner =>
+ _globalInfo.StepRunningDic.TryGetValue(_globalInfo.CurrentScope, out var runner) ? runner : null;
+ private SystemConfig? CurrentConfig =>
+ _globalInfo.ConfigDic.TryGetValue(_globalInfo.CurrentScope, out var config) ? config : null;
+ public string RunState
+ {
+ get => CurrentContext?.RunState ?? "运行";
+ set
+ {
+ if (CurrentContext != null && CurrentContext.RunState != value)
+ {
+ CurrentContext.RunState = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ public bool SingleStep
+ {
+ get => CurrentContext?.SingleStep ?? false;
+ set
+ {
+ if (CurrentContext != null && CurrentContext.SingleStep != value)
+ {
+ CurrentContext.SingleStep = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ public PackIconKind RunIcon
+ {
+ get => CurrentContext?.RunIcon ?? PackIconKind.Play;
+ set
+ {
+ if (CurrentContext != null && CurrentContext.RunIcon != value)
+ {
+ CurrentContext.RunIcon = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+ #endregion
+
+ #region 命令
+ public ICommand LeftDrawerOpenCommand { get; set; }
+ public ICommand MinimizeCommand { get; set; }
+ public ICommand MaximizeCommand { get; set; }
+ public ICommand CloseCommand { get; set; }
+ public ICommand NavigateCommand { get; set; }
+ public ICommand LoadCommand { get; set; }
+ public ICommand RefreshCommand { get; set; }
+ public ICommand DestroyCommand { get; set; }
+ public ICommand RunningCommand { get; set; }
+ public ICommand RunSingleCommand { get; set; }
+ public ICommand RestorationCommand { get; set; }
+ public ICommand RunAbnormalStepsCommand { get; set; }
+ public ICommand SaveAsCommand { get; set; }
+ public ICommand SaveCommand { get; set; }
+ public ICommand OpenCommand { get; set; }
+ public ICommand NewCommand { get; set; }
+ public ICommand SetDefaultCommand { get; set; }
+ public ICommand ShowDialogManagerViewCommand { get; set; }
+ public ICommand SelectCANSignalMonitorCommand { get; set; }
+ public ICommand SelectCANMessageCommand { get; set; }
+ public ICommand MonitorValueSettingCommand { get; set; }
+ public ICommand GetFileStringCommand { get; set; }
+ public ICommand SilenceBuzzerCommand { get; set; }
+ #endregion
+
+ public ShellViewModel(IContainerProvider containerProvider)
+ {
+ _containerProvider = containerProvider;
+ _dialogService= containerProvider.Resolve();
+ _globalInfo = containerProvider.Resolve();
+ _eventAggregator = containerProvider.Resolve();
+ _regionManager = containerProvider.Resolve();
+ _notificationManager = containerProvider.Resolve();
+ _moduleManager = containerProvider.Resolve();
+ LeftDrawerOpenCommand = new DelegateCommand(LeftDrawerOpen);
+ ShowDialogManagerViewCommand = new DelegateCommand(ShowDialogManagerView);
+ MinimizeCommand = new DelegateCommand(MinimizeWindow);
+ MaximizeCommand = new DelegateCommand(MaximizeWindow);
+ CloseCommand = new DelegateCommand(CloseWindow);
+ NavigateCommand = new DelegateCommand(Navigate);
+ LoadCommand = new DelegateCommand(Load);
+ RefreshCommand = new DelegateCommand(OnRefresh);
+ DestroyCommand = new DelegateCommand(OnDestroy);
+ RunningCommand = new DelegateCommand(OnRunning);
+ RunSingleCommand = new DelegateCommand(RunSingle);
+ RestorationCommand = new AsyncDelegateCommand(OnRestoration);
+ RunAbnormalStepsCommand = new AsyncDelegateCommand(OnRunAbnormalSteps);
+ NewCommand = new DelegateCommand(New);
+ OpenCommand = new AsyncDelegateCommand(Open);
+ SaveAsCommand = new DelegateCommand(SaveAs);
+ SaveCommand = new DelegateCommand(Save);
+ SetDefaultCommand = new DelegateCommand(SetDefault);
+ SelectCANSignalMonitorCommand = new DelegateCommand(SelectCANSignalMonitor);
+ SelectCANMessageCommand = new DelegateCommand(SelectCANMessage);
+ MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
+ GetFileStringCommand = new DelegateCommand(GetFileString);
+ SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
+
+ _globalInfo.ContextDic.Add("default", new ScopedContext());
+
+ _eventAggregator.GetEvent().Subscribe(() =>
+ {
+ Application.Current.MainWindow.Show();
+ _regionManager.RequestNavigate("ShellViewManager", "MainView");
+ });
+
+ _eventAggregator.GetEvent().Subscribe(UpdateRunIcon);
+
+ _globalInfo.ScopeChanged += (s, e) =>
+ {
+ RefreshAllContextProperties();
+ };
+
+ // 💡 初始化轻量级 UI 定时器:每 100 毫秒刷新一次当前可见工位的界面时间
+ _uiRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
+ _uiRefreshTimer.Tick += (s, e) => RaisePropertyChanged(nameof(RunningTime));
+ _uiRefreshTimer.Start();
+ }
+
+
+ private void RefreshAllContextProperties()
+ {
+ RaisePropertyChanged(nameof(RunState));
+ RaisePropertyChanged(nameof(SingleStep));
+ RaisePropertyChanged(nameof(RunIcon));
+ RaisePropertyChanged(nameof(RunningTime));
+ RaisePropertyChanged(nameof(IsTerminate));
+ }
+
+ #region 命令处理与事件
+
+ private async Task SilenceBuzzer()
+ {
+ _eventAggregator.GetEvent().Publish(_globalInfo.CurrentScope);
+
+ }
+ private void GetFileString()
+ {
+ var openFileDialog = new OpenFileDialog
+ {
+ Title = "选择文件",
+ Filter = "所有文件 (*.*)|*.*",
+ };
+ if (openFileDialog.ShowDialog() == true)
+ {
+ string filePath = openFileDialog.FileName;
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Clipboard.SetDataObject(filePath);
+ });
+ }
+ }
+ private void MonitorValueSetting()
+ {
+ if ( _globalInfo.CurrentScope == "default")
+ {
+ LoggerHelper.Warn("当前作用域未配置 CAN 设备,无法打开报文选择窗口。");
+ return;
+ }
+ _dialogService.ShowDialog("ValueLimitView", new DialogParameters
+ {
+ { "Scope", _globalInfo.ScopeDic[_globalInfo.CurrentScope] }
+ }, _ => { });
+ }
+
+ private void SelectCANSignalMonitor()
+ {
+ var canfd = CurrentConfig?.CANFD;
+ if (canfd == null || _globalInfo.CurrentScope == "default")
+ {
+ return;
+ }
+
+ _dialogService.ShowDialog("CollectCANSignalSettingView", new DialogParameters
+ {
+ { "SystemConfig", CurrentConfig }
+ }, _ => { });
+ }
+
+ private void SelectCANMessage()
+ {
+ var canfd = CurrentConfig?.CANFD;
+ if (canfd == null||_globalInfo.CurrentScope=="default")
+ {
+ LoggerHelper.Warn("当前作用域未配置 CAN 设备,无法打开报文选择窗口。");
+ return;
+ }
+
+ _dialogService.ShowDialog("SelectCanMessageView", new DialogParameters
+ {
+ { "CANFD", canfd }
+ }, _ => { });
+ }
+
+ private void ShowDialogManagerView()
+ {
+ _eventAggregator.GetEvent().Publish();
+ }
+ private async void OnRunning()
+ {
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+ StepRunning? targetRunner = CurrentRunner;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null || targetRunner == null)
+ {
+ return;
+ }
+
+ if (targetContext.RunState == "运行")
+ {
+ targetContext.SingleStep = false;
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 运行命令");
+
+ targetContext.RunState = "暂停";
+ targetContext.RunIcon = PackIconKind.Pause;
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+
+ if (targetContext.IsStop == null)
+ {
+ targetContext.IsStop = false;
+
+ // 💡 启动或恢复:启动属于该快照工位独立的 SW 计时器
+ targetContext.SW.Start();
+
+ if (!_executionTasks.TryGetValue(runningScope, out var existingTask) || existingTask == null)
+ {
+ existingTask = targetRunner.ExecuteSteps(targetContext.Program, cancellationToken: targetRunner.stepCTS.Token);
+ _executionTasks[runningScope] = existingTask;
+ }
+
+ try
+ {
+ await existingTask;
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 异常中止: {ex.Message}");
+ }
+
+ // 💡 测试正常完毕或取消:停止当前工位的 SW,并将最终时间固化到 RunningTime 字段中
+ targetContext.SW.Stop();
+ targetContext.RunningTime = targetContext.SW.Elapsed;
+
+ targetContext.RunState = "运行";
+ targetContext.RunIcon = PackIconKind.Play;
+ targetContext.IsStop = null;
+
+ if (targetRunner.stepCTS.IsCancellationRequested)
+ {
+ targetRunner.stepCTS = new CancellationTokenSource();
+ _executionTasks.TryRemove(runningScope, out _);
+ }
+ else if (_executionTasks.TryGetValue(runningScope, out var currentTask) && currentTask != null && currentTask.IsCompleted)
+ {
+ targetContext.IsTerminate = true;
+ _executionTasks.TryRemove(runningScope, out _);
+ }
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+ else if (targetContext.IsStop == true)
+ {
+ targetContext.IsStop = false;
+ // 💡 从暂停中恢复,继续累加当前工位计时
+ targetContext.SW.Start();
+ }
+ }
+ else // 用户点击了暂停
+ {
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 点击工位 [{runningScope}] 暂停命令");
+ targetContext.SingleStep = true;
+ targetContext.IsStop = true;
+ targetContext.RunState = "运行";
+ targetContext.RunIcon = PackIconKind.Play;
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+ }
+
+ private async void RunSingle()
+ {
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+ StepRunning? targetRunner = CurrentRunner;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null || targetRunner == null)
+ {
+ return;
+ }
+
+ if (targetContext.RunState == "运行")
+ {
+ targetContext.SingleStep = true;
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 单步执行命令");
+
+ targetContext.RunState = "暂停";
+ targetContext.RunIcon = PackIconKind.Pause;
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+
+ if (targetContext.IsStop == null)
+ {
+ targetContext.IsStop = false;
+
+ // 💡 单步启动:让专属工位的 SW 跑起来
+ targetContext.SW.Start();
+
+ if (!_executionTasks.TryGetValue(runningScope, out var existingTask) || existingTask == null)
+ {
+ existingTask = targetRunner.ExecuteSteps(targetContext.Program, cancellationToken: targetRunner.stepCTS.Token);
+ _executionTasks[runningScope] = existingTask;
+ }
+
+ try
+ {
+ await existingTask;
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 单步执行异常: {ex.Message}");
+ }
+
+ // 💡 单步单步完成后会被 StepRunning 挂起,在此暂时停止 SW
+ targetContext.SW.Stop();
+ targetContext.RunningTime = targetContext.SW.Elapsed;
+
+ targetContext.RunState = "运行";
+ targetContext.RunIcon = PackIconKind.Play;
+ targetContext.IsStop = null;
+
+ if (targetRunner.stepCTS.IsCancellationRequested)
+ {
+ targetRunner.stepCTS = new CancellationTokenSource();
+ _executionTasks.TryRemove(runningScope, out _);
+ }
+ else if (_executionTasks.TryGetValue(runningScope, out var currentTask) && currentTask != null && currentTask.IsCompleted)
+ {
+ targetContext.IsTerminate = true;
+ _executionTasks.TryRemove(runningScope, out _);
+ }
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+ else if (targetContext.IsStop == true)
+ {
+ targetContext.IsStop = false;
+ // 💡 继续单步,继续计时
+ targetContext.SW.Start();
+ }
+ }
+ }
+
+ private async Task OnRestoration()
+ {
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+ StepRunning? targetRunner = CurrentRunner;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null || targetRunner == null)
+ {
+ return;
+ }
+
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 复位命令");
+
+ _executionTasks.TryGetValue(runningScope, out var currentTask);
+ _errorExecutionTasks.TryGetValue(runningScope, out var errorTask);
+
+ if (currentTask != null || errorTask != null)
+ {
+ targetRunner.stepCTS.Cancel();
+ targetRunner.errorStepCTS.Cancel();
+
+ await Task.Delay(200);
+
+ // 💡 彻底复位:停止当前工位的计时器,并且完全归零(Reset)
+ targetContext.SW.Reset();
+ targetContext.RunningTime = TimeSpan.Zero;
+
+ targetContext.IsStop = null;
+ targetRunner.ResetAllStepStatus(targetContext.Program.StepCollection);
+ targetRunner.ResetAllStepStatus(targetContext.Program.ErrorStepCollection);
+ targetContext.IsTerminate = false;
+ }
+ else
+ {
+ targetRunner.stepCTS = new CancellationTokenSource();
+ targetRunner.errorStepCTS = new CancellationTokenSource();
+
+ // 💡 无任务状态下的直接归零
+ targetContext.SW.Reset();
+ targetContext.RunningTime = TimeSpan.Zero;
+
+ targetContext.IsStop = null;
+ targetRunner.ResetAllStepStatus(targetContext.Program.StepCollection);
+ targetRunner.ResetAllStepStatus(targetContext.Program.ErrorStepCollection);
+ targetContext.IsTerminate = false;
+ }
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+
+ private async Task OnRunAbnormalSteps()
+ {
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+ StepRunning? targetRunner = CurrentRunner;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null || targetRunner == null)
+ {
+ return;
+ }
+
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 异常流程命令");
+
+ // 💡 异常测试启动计时
+ targetContext.SW.Start();
+
+ if (!_errorExecutionTasks.TryGetValue(runningScope, out var existingErrorTask) || existingErrorTask == null)
+ {
+ existingErrorTask = targetRunner.ExecuteErrorSteps(targetContext.Program, cancellationToken: targetRunner.errorStepCTS.Token);
+ _errorExecutionTasks[runningScope] = existingErrorTask;
+ }
+
+ try
+ {
+ await existingErrorTask;
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 异常流程执行出错: {ex.Message}");
+ }
+
+ // 💡 异常测试结束停止计时
+ targetContext.SW.Stop();
+ targetContext.RunningTime = targetContext.SW.Elapsed;
+
+ if (targetRunner.errorStepCTS.IsCancellationRequested)
+ {
+ targetRunner.errorStepCTS = new CancellationTokenSource();
+ _errorExecutionTasks.TryRemove(runningScope, out _);
+ }
+ else if (_errorExecutionTasks.TryGetValue(runningScope, out var currentErrorTask) && currentErrorTask != null && currentErrorTask.IsCompleted)
+ {
+ targetContext.IsTerminate = true;
+ _errorExecutionTasks.TryRemove(runningScope, out _);
+ }
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+
+ private void SetDefault()
+ {
+ if (CurrentConfig == null) return;
+ // 💡 1. 抓取触发瞬间的 Scope 与 Context 快照
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
+
+ if (targetContext.CurrentFilePath != null)
+ {
+ CurrentConfig.DefaultProgramFilePath = targetContext.CurrentFilePath;
+ ConfigService.Save(CurrentConfig);
+ LoggerHelper.SuccessWithNotify(runningScope, $"工位 [{runningScope}] 已成功将当前程序设为默认启动程序");
+ }
+ }
+
+ private void New()
+ {
+ // 💡 1. 抓取快照
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
+ CurrentConfig.CurrentACPFile = "新建ACP文件";
+ // 💡 2. 严格对快照隔离的 Context 数据进行清理,不影响其他工位
+ targetContext.CurrentFilePath = null;
+ targetContext.Program.Parameters.Clear();
+ targetContext.Program.StepCollection.Clear();
+ targetContext.Program.ErrorStepCollection.Clear();
+ foreach (var item in CurrentConfig.ParameterList)
+ {
+ var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
+ if (param != null)
+ {
+ item.Value = param.Value;
+ }
+ targetContext.Program.Parameters.Add(item);
+ }
+ LoggerHelper.InfoWithNotify(runningScope, $"工位 [{runningScope}] 创建了空程序文件");
+
+ // 如果当前正看着该工位,刷新 UI 显示
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+
+ private async Task Open(string filePath = null)
+ {
+ // 💡 1. 抓取进方法瞬间的快照,锁死上下文
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
+
+ try
+ {
+ // 如果没有传路径,弹出文件选择对话框(WPF 对话框是模态的,会阻塞当前 UI 线程)
+ if (string.IsNullOrEmpty(filePath))
+ {
+ var openFileDialog = new Microsoft.Win32.OpenFileDialog
+ {
+ Filter = "ACP程序文件|*.ACP|所有文件|*.*",
+ Title = $"工位 [{runningScope}] 打开程序",
+ };
+
+ if (openFileDialog.ShowDialog() != true)
+ return; // 用户取消选择
+
+ filePath = openFileDialog.FileName;
+ }
+
+ // 确认文件存在
+ if (!File.Exists(filePath))
+ {
+ LoggerHelper.ErrorWithNotify(runningScope, $"文件不存在: {filePath}");
+ return;
+ }
+
+ // 读取 JSON 文件
+ string json = File.ReadAllText(filePath);
+
+ // 反序列化为 ProgramVM
+ var program = Newtonsoft.Json.JsonConvert.DeserializeObject(json);
+
+ if (program == null)
+ {
+ LoggerHelper.WarnWithNotify(runningScope, $"文件格式不正确或为空: {filePath}");
+ return;
+ }
+ CurrentConfig.CurrentACPFile = filePath;
+ // 💡 2. 严格赋值给快照锁定下的当前工位上下文,实现数据完全隔离
+ targetContext.Program.Parameters = program.Parameters;
+ targetContext.Program.StepCollection = program.StepCollection;
+ targetContext.Program.ErrorStepCollection = program.ErrorStepCollection;
+ targetContext.CurrentFilePath = filePath;
+
+ foreach (var item in CurrentConfig.SharedParameterList)
+ {
+ var parameter = targetContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName);
+
+ if (parameter != null)
+ {
+ parameter.Value = item.Value;
+ }
+ }
+
+ LoggerHelper.SuccessWithNotify(runningScope, $"工位 [{runningScope}] 成功打开文件: {filePath}");
+
+ // 💡 3. 安全调用异步复位,确保重置的是对应工位的数据
+ // 注意:由于在 OnRestoration 内部第一行也做了快照拦截,
+ // 如果用户此时正好在看这个工位,它会自动完美执行
+ await OnRestoration();
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.ErrorWithNotify(runningScope, $"工位 [{runningScope}] 打开文件失败: {ex.Message}");
+ }
+ finally
+ {
+ // 💡 4. 异步回归:不管中途用户切去了哪里,回到该工位时及时刷新显示
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+ }
+
+ private void SaveAs()
+ {
+ if (!_globalInfo.IsAdmin) return;
+
+ // 💡 1. 抓取快照
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
+
+ string defaultPath = @"D:\ACP\子程序";
+ if (!Directory.Exists(defaultPath))
+ Directory.CreateDirectory(defaultPath);
+
+ var saveFileDialog = new Microsoft.Win32.SaveFileDialog
+ {
+ Filter = "ACP程序文件|*.ACP|所有文件|*.*",
+ Title = $"工位 [{runningScope}] 程序另存为",
+ FileName = "NewProgram.ACP",
+ InitialDirectory = defaultPath
+ };
+
+ if (saveFileDialog.ShowDialog() == true)
+ {
+ // 💡 2. 将新路径安全写入快照工位
+ CurrentConfig.CurrentACPFile = saveFileDialog.FileName;
+ targetContext.CurrentFilePath = saveFileDialog.FileName;
+
+ // 💡 3. 调用你的底层文件保存逻辑,传入快照工位的 Program 模型
+ SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program);
+
+ LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 另存为文件成功: {saveFileDialog.FileName}");
+
+ if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
+ }
+ }
+
+ private void Save()
+ {
+ if (!_globalInfo.IsAdmin) return;
+
+ // 💡 1. 抓取快照
+ string runningScope = _globalInfo.CurrentScope;
+ ScopedContext? targetContext = CurrentContext;
+
+ if (runningScope == "default" || string.IsNullOrEmpty(runningScope) || targetContext == null) return;
+
+ // 💡 2. 判断当前工位是否有历史路径
+ if (targetContext.CurrentFilePath == null)
+ {
+ SaveAs();
+ return;
+ }
+
+ // 💡 3. 持久化当前快照工位的 Program
+ SaveProgramToFile(targetContext.CurrentFilePath, targetContext.Program);
+ LoggerHelper.SuccessWithNotify(_globalInfo.CurrentScope,$"工位 [{runningScope}] 程序保存成功!");
+ }
+
+ // 💡 辅助方法:建议将你的通用序列化落盘代码调整为接收 ProgramVM 参数,提高复用性
+ private void SaveProgramToFile(string filePath, ProgramVM ProgramVM)
+ {
+ try
+ {
+ string json = Newtonsoft.Json.JsonConvert.SerializeObject(ProgramVM, Newtonsoft.Json.Formatting.Indented);
+ File.WriteAllText(filePath, json);
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,$"写入程序文件失败: {ex.Message}");
+ }
+ }
+
+ private void UpdateRunIcon(string obj)
+ {
+ CurrentContext?.SW.Stop();
+ CurrentContext.RunningTime = CurrentContext.SW.Elapsed;
+ RunIcon = obj switch
+ {
+ "Play" => PackIconKind.Play,
+ "Pause" => PackIconKind.Pause,
+ _ => RunIcon
+ };
+ }
+
+ private void OnRefresh()
+ {
+ _eventAggregator.GetEvent().Publish("");
+ _globalInfo.CurrentScope = "default";
+ }
+ private void OnDestroy()
+ {
+ if (_globalInfo.CurrentScope == "default" || string.IsNullOrEmpty(_globalInfo.CurrentScope)) return;
+
+ string targetRegionName = _globalInfo.CurrentScope;
+
+ // 💡 物理销毁工位时,顺便清理任务字典,防止内存泄漏
+ _executionTasks.TryRemove(targetRegionName, out _);
+ _errorExecutionTasks.TryRemove(targetRegionName, out _);
+
+ if (_regionManager.Regions.ContainsRegionWithName(targetRegionName))
+ {
+ var region = _regionManager.Regions[targetRegionName];
+ var viewsToDestroy = region.Views.ToList();
+
+ foreach (var view in viewsToDestroy)
+ {
+ if (view is FrameworkElement element)
+ {
+ var viewModel = element.DataContext;
+ region.Remove(view);
+ if (viewModel is IDisposable disposableVM) disposableVM.Dispose();
+ }
+ }
+ }
+
+ var parameters = new NavigationParameters { { "Name", targetRegionName } };
+ _regionManager.RequestNavigate(targetRegionName, "ProtocolStartView", parameters);
+ _eventAggregator.GetEvent().Publish("");
+ }
+ private void Load()
+ {
+ _notificationManager.ShowAsync(new NotificationContent { Title = "登录成功", Message = "", Type = NotificationType.Success });
+ //默认导航到主界面
+ Type moduleAType = typeof(MainModule.MainModule);
+ _moduleManager.LoadModule(moduleAType.Name);
+ }
+ private void Navigate(string content)
+ {
+ switch (content)
+ {
+ case "主界面":
+ if (_globalInfo.CurrentScope == "default" || string.IsNullOrEmpty(_globalInfo.CurrentScope))
+ {
+ _regionManager.RequestNavigate("ShellViewManager", "MainView");
+ break;
+ }
+ var TestingRegion = _globalInfo.CurrentScope;
+ if (!_regionManager.Regions.ContainsRegionWithName(TestingRegion)) break;
+ _regionManager.RequestNavigate(TestingRegion, "AutomatedTestingView");
+ break;
+ case "监控界面":
+ // 仅当某个工位被选中(九宫格已展开/选择)时才跳转,默认 default 跳过。
+ if (_globalInfo.CurrentScope == "default" || string.IsNullOrEmpty(_globalInfo.CurrentScope)) break;
+
+ // 记录界面不是填到全局的 ShellViewManager,而是填到该工位专属的 region。
+ var monitorRegion = _globalInfo.CurrentScope;
+ if (!_regionManager.Regions.ContainsRegionWithName(monitorRegion)) break;
+
+ var monitorParameters = new NavigationParameters { { "Name", monitorRegion } };
+ _regionManager.RequestNavigate(monitorRegion, "MonitorView", monitorParameters);
+ IsLeftDrawerOpen = false;
+ break;
+ case "记录界面":
+ if (_globalInfo.CurrentScope == "default" || string.IsNullOrEmpty(_globalInfo.CurrentScope)) break;
+
+ // 记录界面不是填到全局的 ShellViewManager,而是填到该工位专属的 region。
+ var recordRegion = _globalInfo.CurrentScope;
+ if (!_regionManager.Regions.ContainsRegionWithName(recordRegion)) break;
+
+ var recordParameters = new NavigationParameters { { "Name", recordRegion } };
+ _regionManager.RequestNavigate(recordRegion, "RecordView", recordParameters);
+ IsLeftDrawerOpen = false;
+ break;
+ case "设置界面":
+ if (_globalInfo.CurrentScope == "default" || string.IsNullOrEmpty(_globalInfo.CurrentScope)) break;
+
+ // 记录界面不是填到全局的 ShellViewManager,而是填到该工位专属的 region。
+ var settingRegion = _globalInfo.CurrentScope;
+ if (!_regionManager.Regions.ContainsRegionWithName(settingRegion)) break;
+
+ var settingParameters = new NavigationParameters { { "Name", settingRegion } };
+ _regionManager.RequestNavigate(settingRegion, "SettingView", settingParameters);
+ IsLeftDrawerOpen = false;
+ break;
+ case "台架移动界面":
+ if (_globalInfo.CurrentScope != "default") return;
+ _regionManager.RequestNavigate("ShellViewManager", "BenchMovementView");
+ break;
+ case "曲线回读界面":
+ if (_globalInfo.CurrentScope != "default") return;
+ _regionManager.RequestNavigate("ShellViewManager", "CurveRecallView");
+ break;
+ case "测试报告导出界面":
+ if (_globalInfo.CurrentScope != "default") return;
+ _regionManager.RequestNavigate("ShellViewManager", "ExportView");
+ break;
+ }
+ }
+
+ private void LeftDrawerOpen()
+ {
+ IsLeftDrawerOpen = true;
+ }
+
+ private void MinimizeWindow(Window window)
+ {
+ if (window != null)
+ window.WindowState = WindowState.Minimized;
+ }
+
+ private void MaximizeWindow(Window window)
+ {
+ if (window != null)
+ {
+ window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ }
+ }
+
+ private void CloseWindow(Window window)
+ {
+ window?.Close();
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/ACP/Views/Dialogs/MessageBoxView.xaml b/ACP/Views/Dialogs/MessageBoxView.xaml
new file mode 100644
index 0000000..99cb6f8
--- /dev/null
+++ b/ACP/Views/Dialogs/MessageBoxView.xaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ACP/Views/Dialogs/MessageBoxView.xaml.cs b/ACP/Views/Dialogs/MessageBoxView.xaml.cs
new file mode 100644
index 0000000..a19f560
--- /dev/null
+++ b/ACP/Views/Dialogs/MessageBoxView.xaml.cs
@@ -0,0 +1,36 @@
+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 ACP.Views.Dialogs
+{
+ ///
+ /// MessageBoxView.xaml 的交互逻辑
+ ///
+ public partial class MessageBoxView : UserControl
+ {
+ public MessageBoxView()
+ {
+ InitializeComponent();
+ }
+
+ private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ Window.GetWindow(this)?.DragMove();
+ }
+ }
+ }
+}
diff --git a/ACP/Views/LoginModuleView.xaml b/ACP/Views/LoginModuleView.xaml
new file mode 100644
index 0000000..d546077
--- /dev/null
+++ b/ACP/Views/LoginModuleView.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ACP/Views/LoginModuleView.xaml.cs b/ACP/Views/LoginModuleView.xaml.cs
new file mode 100644
index 0000000..62a5707
--- /dev/null
+++ b/ACP/Views/LoginModuleView.xaml.cs
@@ -0,0 +1,38 @@
+using UIShare.PubEvent;
+using MahApps.Metro.Controls;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+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.Shapes;
+using Path = System.IO.Path;
+
+namespace ACP.Views
+{
+ ///
+ /// Login.xaml 的交互逻辑
+ ///
+ public partial class LoginModuleView : MetroWindow
+ {
+ public LoginModuleView(IEventAggregator eventAggregator)
+ {
+ InitializeComponent();
+ //订阅登录成功事件
+ eventAggregator.GetEvent().Subscribe(() =>
+ {
+ this.Close();
+ });
+ }
+ }
+}
diff --git a/ACP/Views/ShellView.xaml b/ACP/Views/ShellView.xaml
new file mode 100644
index 0000000..ac33618
--- /dev/null
+++ b/ACP/Views/ShellView.xaml
@@ -0,0 +1,400 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ACP/Views/ShellView.xaml.cs b/ACP/Views/ShellView.xaml.cs
new file mode 100644
index 0000000..5b9c27c
--- /dev/null
+++ b/ACP/Views/ShellView.xaml.cs
@@ -0,0 +1,51 @@
+using UIShare.PubEvent;
+using Prism.Events;
+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.Shapes;
+
+namespace ACP.Views
+{
+ ///
+ /// ShellView.xaml 的交互逻辑
+ ///
+ public partial class ShellView : Window
+ {
+ public ShellView(IEventAggregator eventAggregator)
+ {
+ InitializeComponent();
+ //注册灰度遮罩层
+ eventAggregator.GetEvent().Subscribe(ShowOverlay);
+ eventAggregator.GetEvent().Subscribe(ShowWaitinglay);
+ }
+
+ private void ShowWaitinglay(bool arg)
+ {
+ Waitinglay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void ShowOverlay(bool arg)
+ {
+ Overlay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+
+ private void ColorZone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ Window.GetWindow(this)?.DragMove();
+ }
+ }
+ }
+}
diff --git a/BenchMovementModule/BenchMovementModule.cs b/BenchMovementModule/BenchMovementModule.cs
new file mode 100644
index 0000000..6041442
--- /dev/null
+++ b/BenchMovementModule/BenchMovementModule.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+using BenchMovementModule.Views;
+
+namespace BenchMovementModule
+{
+ public class BenchMovementModule : IModule
+ {
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ IRegionManager regionManager = containerProvider.Resolve();
+ regionManager.RegisterViewWithRegion("ShellViewManager", typeof(BenchMovementView));
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ containerRegistry.RegisterForNavigation("BenchMovementView");
+ }
+ }
+}
diff --git a/BenchMovementModule/BenchMovementModule.csproj b/BenchMovementModule/BenchMovementModule.csproj
new file mode 100644
index 0000000..3df7504
--- /dev/null
+++ b/BenchMovementModule/BenchMovementModule.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
diff --git a/BenchMovementModule/HardwareDrive/GantryControlBase.cs b/BenchMovementModule/HardwareDrive/GantryControlBase.cs
new file mode 100644
index 0000000..4240e31
--- /dev/null
+++ b/BenchMovementModule/HardwareDrive/GantryControlBase.cs
@@ -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 ConnectAsync(CancellationToken ct = default)
+ {
+ return await _device.ConnectAsync(ct);
+ }
+
+ public void Close()
+ {
+ _device.Close();
+ }
+
+ ///
+ /// 轴使能控制
+ ///
+ 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);
+ }
+
+ ///
+ /// 左右移动(X轴)
+ ///
+ /// 目标绝对位置(脉冲数/单位值)
+ /// 运行速度
+ 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);
+ }
+
+ ///
+ /// 上下移动(Z轴)
+ ///
+ /// 目标绝对位置
+ /// 运行速度
+ 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);
+ }
+
+ ///
+ /// 前后移动(Y轴 - 预留拓展)
+ ///
+ 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);
+ }
+
+ ///
+ /// 全局回零
+ ///
+ public async Task HomeAsync(CancellationToken ct = default)
+ {
+ await TriggerCoilAsync(ADDR_M_HOME, ct);
+ }
+
+ ///
+ /// 轴停止
+ ///
+ /// 1:X轴, 2:Y轴, 3:Z轴, 0:全停
+ 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 内部工具方法
+
+ ///
+ /// 触发一个点动信号(置 1 后,延时 100ms 自动置 0)
+ ///
+ 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);
+ }
+
+ ///
+ /// 将 32 位整型数据转换为 Modbus 的 2 个 16 位无符号整数(高低字转换)
+ ///
+ 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
+ }
+}
diff --git a/BenchMovementModule/HardwareDrive/GantryControlRtu.cs b/BenchMovementModule/HardwareDrive/GantryControlRtu.cs
new file mode 100644
index 0000000..8ce5fa3
--- /dev/null
+++ b/BenchMovementModule/HardwareDrive/GantryControlRtu.cs
@@ -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);
+ }
+ }
+}
diff --git a/BenchMovementModule/HardwareDrive/GantryControlTcp.cs b/BenchMovementModule/HardwareDrive/GantryControlTcp.cs
new file mode 100644
index 0000000..dae9bd9
--- /dev/null
+++ b/BenchMovementModule/HardwareDrive/GantryControlTcp.cs
@@ -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);
+ }
+ }
+}
diff --git a/BenchMovementModule/ViewModels/BenchMovementViewModel.cs b/BenchMovementModule/ViewModels/BenchMovementViewModel.cs
new file mode 100644
index 0000000..d1690c3
--- /dev/null
+++ b/BenchMovementModule/ViewModels/BenchMovementViewModel.cs
@@ -0,0 +1,522 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using System.Windows.Threading;
+using DeviceCommand.Devices;
+using Logger;
+using Prism.Ioc;
+using UIShare.GlobalVariable;
+using UIShare.ViewModelBase;
+
+namespace BenchMovementModule.ViewModels
+{
+ public class BenchMovementViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
+ {
+ #region 属性
+
+ public bool KeepAlive => true;
+
+ // ===== GantryControlTcp 实例(从 DeviceManager 获取)=====
+ private GantryControlTcp? _gantry;
+ private DeviceManager? _deviceManager;
+ private readonly GlobalInfo _globalInfo;
+
+ // ===== 连接状态 =====
+ private bool _isConnected;
+ public bool IsConnected
+ {
+ get => _isConnected;
+ set => SetProperty(ref _isConnected, value);
+ }
+
+ private string _connectionStatus = "未连接";
+ public string ConnectionStatus
+ {
+ get => _connectionStatus;
+ set => SetProperty(ref _connectionStatus, value);
+ }
+
+ // ===== 6轴绝对位置显示(相对原点的实时位置,只读)=====
+ private int _absPos1;
+ public int AbsPos1 { get => _absPos1; set => SetProperty(ref _absPos1, value); }
+
+ private int _absPos2;
+ public int AbsPos2 { get => _absPos2; set => SetProperty(ref _absPos2, value); }
+
+ private int _absPos3;
+ public int AbsPos3 { get => _absPos3; set => SetProperty(ref _absPos3, value); }
+
+ private int _absPos4;
+ public int AbsPos4 { get => _absPos4; set => SetProperty(ref _absPos4, value); }
+
+ private int _absPos5;
+ public int AbsPos5 { get => _absPos5; set => SetProperty(ref _absPos5, value); }
+
+ private int _absPos6;
+ public int AbsPos6 { get => _absPos6; set => SetProperty(ref _absPos6, value); }
+
+ // ===== 寸动目标位置 =====
+ private int _targetX;
+ public int TargetX { get => _targetX; set => SetProperty(ref _targetX, value); }
+
+ private int _targetY;
+ public int TargetY { get => _targetY; set => SetProperty(ref _targetY, value); }
+
+ private int _targetZ;
+ public int TargetZ { get => _targetZ; set => SetProperty(ref _targetZ, value); }
+
+ // ===== 扫描参数 =====
+ private int _scanStepX = 1000;
+ public int ScanStepX { get => _scanStepX; set => SetProperty(ref _scanStepX, value); }
+
+ private int _scanStartX;
+ public int ScanStartX { get => _scanStartX; set => SetProperty(ref _scanStartX, value); }
+
+ private int _scanEndX = 50000;
+ public int ScanEndX { get => _scanEndX; set => SetProperty(ref _scanEndX, value); }
+
+ private int _scanStepY = 1000;
+ public int ScanStepY { get => _scanStepY; set => SetProperty(ref _scanStepY, value); }
+
+ private int _scanStartY;
+ public int ScanStartY { get => _scanStartY; set => SetProperty(ref _scanStartY, value); }
+
+ private int _scanEndY = 50000;
+ public int ScanEndY { get => _scanEndY; set => SetProperty(ref _scanEndY, value); }
+
+ private int _scanStepZ = 100;
+ public int ScanStepZ { get => _scanStepZ; set => SetProperty(ref _scanStepZ, value); }
+
+ private int _scanStartZ;
+ public int ScanStartZ { get => _scanStartZ; set => SetProperty(ref _scanStartZ, value); }
+
+ private int _scanEndZ = 20000;
+ public int ScanEndZ { get => _scanEndZ; set => SetProperty(ref _scanEndZ, value); }
+
+ // ===== 速度设定(脉冲)=====
+ private int _speedX = 5000;
+ public int SpeedX { get => _speedX; set => SetProperty(ref _speedX, value); }
+
+ private int _speedY = 5000;
+ public int SpeedY { get => _speedY; set => SetProperty(ref _speedY, value); }
+
+ private int _speedZ = 2000;
+ public int SpeedZ { get => _speedZ; set => SetProperty(ref _speedZ, value); }
+
+ private int _homeSpeed = 3000;
+ public int HomeSpeed { get => _homeSpeed; set => SetProperty(ref _homeSpeed, value); }
+
+ // ===== 零点设定 =====
+ private int _homeX;
+ public int HomeX { get => _homeX; set => SetProperty(ref _homeX, value); }
+
+ private int _homeY;
+ public int HomeY { get => _homeY; set => SetProperty(ref _homeY, value); }
+
+ private int _homeZ;
+ public int HomeZ { get => _homeZ; set => SetProperty(ref _homeZ, value); }
+
+ #endregion
+
+ #region 命令
+ public ICommand ConnectCommand { get; }
+ public ICommand DisconnectCommand { get; }
+ public ICommand MoveXCommand { get; }
+ public ICommand MoveYCommand { get; }
+ public ICommand MoveZCommand { get; }
+ public ICommand MoveAllCommand { get; }
+ public ICommand GoHomeCommand { get; }
+ public ICommand EmergencyStopCommand { get; }
+ public ICommand StopXCommand { get; }
+ public ICommand StopYCommand { get; }
+ public ICommand StopZCommand { get; }
+ public ICommand SetSpeedCommand { get; }
+ public ICommand SetHomeCommand { get; }
+ public ICommand ReadStatusCommand { get; }
+ #endregion
+
+ #region 私有字段
+ private readonly DispatcherTimer _pollTimer;
+ private bool _isInitiated = false;
+ #endregion
+
+ public BenchMovementViewModel(IContainerProvider containerProvider) : base(containerProvider)
+ {
+ _globalInfo = containerProvider.Resolve();
+ ConnectCommand = new DelegateCommand(OnConnect);
+ DisconnectCommand = new DelegateCommand(OnDisconnect);
+ MoveXCommand = new DelegateCommand(OnMoveX);
+ MoveYCommand = new DelegateCommand(OnMoveY);
+ MoveZCommand = new DelegateCommand(OnMoveZ);
+ MoveAllCommand = new DelegateCommand(OnMoveAll);
+ GoHomeCommand = new DelegateCommand(OnGoHome);
+ EmergencyStopCommand = new DelegateCommand(OnEmergencyStop);
+ StopXCommand = new DelegateCommand(OnStopX);
+ StopYCommand = new DelegateCommand(OnStopY);
+ StopZCommand = new DelegateCommand(OnStopZ);
+ SetSpeedCommand = new DelegateCommand(OnSetSpeed);
+ SetHomeCommand = new DelegateCommand(OnSetHome);
+ ReadStatusCommand = new DelegateCommand(OnReadStatus);
+
+ // 位置轮询定时器(500ms)
+ _pollTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(500)
+ };
+ _pollTimer.Tick += OnPollTick;
+ }
+
+ #region 命令处理
+
+ private async void OnConnect()
+ {
+ 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 OnDisconnect()
+ {
+ 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 async void OnMoveX()
+ {
+ 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 async void OnMoveY()
+ {
+ 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 async void OnMoveZ()
+ {
+ 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 async void OnMoveAll()
+ {
+ 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 async void OnGoHome()
+ {
+ if (!EnsureConnected()) return;
+ try
+ {
+ await _gantry!.HomeAsync();
+ LoggerHelper.Info("[BenchMovement] 回原点已触发");
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.Error($"[BenchMovement] 回原点失败: {ex.Message}");
+ ShowErrorMessageBox($"回原点失败: {ex.Message}", () => { });
+ }
+ }
+
+ private async void OnEmergencyStop()
+ {
+ if (!EnsureConnected()) return;
+ try
+ {
+ await _gantry!.SetEStopStateAsync(true);
+ LoggerHelper.Info("[BenchMovement] 急停已触发");
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.Error($"[BenchMovement] 急停失败: {ex.Message}");
+ }
+ }
+
+ private async void OnStopX()
+ {
+ 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 async void OnStopY()
+ {
+ 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
+
+ #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)
+ {
+ base.OnNavigatedTo(navigationContext);
+ if (!_isInitiated)
+ {
+ _isInitiated = true;
+ InitGantryFromDeviceManager();
+ }
+ }
+
+ ///
+ /// 从 DeviceManager.DeviceMap 中查找 GantryControlTcp 实例,
+ /// 优先使用当前打开的作用域,找不到则取任意已注册作用域。
+ ///
+ 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();
+ _gantry = _deviceManager.DeviceMap.Values.OfType().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()
+ {
+ _pollTimer.Stop();
+ // GantryControlTcp 由 DeviceManager 统一管理生命周期,此处仅停止轮询
+ }
+
+ #endregion
+ }
+}
diff --git a/BenchMovementModule/Views/BenchMovementView.xaml b/BenchMovementModule/Views/BenchMovementView.xaml
new file mode 100644
index 0000000..0b57a9b
--- /dev/null
+++ b/BenchMovementModule/Views/BenchMovementView.xaml
@@ -0,0 +1,394 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/BenchMovementModule/Views/BenchMovementView.xaml.cs b/BenchMovementModule/Views/BenchMovementView.xaml.cs
new file mode 100644
index 0000000..d2c15b9
--- /dev/null
+++ b/BenchMovementModule/Views/BenchMovementView.xaml.cs
@@ -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 BenchMovementModule.Views
+{
+ ///
+ /// BenchMovementView.xaml 的交互逻辑
+ ///
+ public partial class BenchMovementView : UserControl
+ {
+ public BenchMovementView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/CANModule/CANModule.cs b/CANModule/CANModule.cs
new file mode 100644
index 0000000..9944f14
--- /dev/null
+++ b/CANModule/CANModule.cs
@@ -0,0 +1,18 @@
+using CANModule.Views;
+
+namespace CANModule
+{
+ public class CANModule : IModule
+ {
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ containerRegistry.RegisterDialog("SelectCanMessageView");
+ containerRegistry.RegisterDialog("CollectCANSignalSettingView");
+ containerRegistry.RegisterForNavigation("ZLGCANFDView");
+ }
+ }
+}
diff --git a/CANModule/CANModule.csproj b/CANModule/CANModule.csproj
new file mode 100644
index 0000000..3df7504
--- /dev/null
+++ b/CANModule/CANModule.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
diff --git a/CANModule/ViewModels/CollectCANSingalSettingViewModel.cs b/CANModule/ViewModels/CollectCANSingalSettingViewModel.cs
new file mode 100644
index 0000000..80cf9f7
--- /dev/null
+++ b/CANModule/ViewModels/CollectCANSingalSettingViewModel.cs
@@ -0,0 +1,251 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Input;
+using UIShare.GlobalVariable;
+using UIShare.PubEvent;
+using UIShare.UIViewModel;
+using UIShare.ViewModelBase;
+using ZLGUSBCANFD;
+
+namespace CANModule.ViewModels
+{
+ public class CollectCANSignalSettingViewModel : DialogViewModelBase
+ {
+ #region 字段
+ private ZLGCANFD? _canfd;
+ private SystemConfig? _systemConfig;
+ #endregion
+
+ #region 属性
+ public ObservableCollection ConfigurationList
+ {
+ get => _systemConfig?.ConfigurationList ?? new ObservableCollection();
+ }
+
+
+
+ private ObservableCollection _messageList;
+ public ObservableCollection MessageList
+ {
+ get => _messageList;
+ set => SetProperty(ref _messageList, value);
+ }
+
+ private ObservableCollection _signalList;
+ public ObservableCollection SignalList
+ {
+ get => _signalList;
+ set => SetProperty(ref _signalList, value);
+ }
+
+ private string _title = "信号采集设置";
+ public string Title
+ {
+ get => _title;
+ set => SetProperty(ref _title, value);
+ }
+
+ private string _selectedMessage;
+ public string SelectedMessage
+ {
+ get => _selectedMessage;
+ set
+ {
+ if (SetProperty(ref _selectedMessage, value))
+ {
+ if (!string.IsNullOrEmpty(value) && _canfd != null)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (MessageChannel >= 0 && MessageChannel < db.Count)
+ {
+ var targetMsg = db[MessageChannel]
+ .FirstOrDefault(x => $"[0x{x.msg_id:X}]{x.msg_name}" == value);
+
+ if (targetMsg != null && targetMsg.signal_Name != null)
+ {
+ SignalList = new ObservableCollection(targetMsg.signal_Name);
+ }
+ else
+ {
+ SignalList = new ObservableCollection();
+ }
+ }
+ }
+ else
+ {
+ SignalList = new ObservableCollection();
+ }
+ }
+ }
+ }
+
+ private CANSignalConfig _SelectedItem;
+ public CANSignalConfig SelectedItem
+ {
+ get => _SelectedItem;
+ set => SetProperty(ref _SelectedItem, value);
+ }
+
+ private string _selectedSignal;
+ public string SelectedSignal
+ {
+ get => _selectedSignal;
+ set => SetProperty(ref _selectedSignal, value);
+ }
+
+ private int _MessageChannel=-1;
+ public int MessageChannel
+ {
+ get => _MessageChannel;
+ set
+ {
+ if (_MessageChannel != value)
+ {
+ _MessageChannel = value;
+
+ SelectedMessage = null;
+ SelectedSignal = null;
+ SignalList = new ObservableCollection();
+
+ if (_canfd != null)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (db != null && value >= 0 && value < db.Count)
+ {
+ MessageList = new ObservableCollection(
+ db[value].Select(s => $"[0x{s.msg_id:X}]{s.msg_name}")
+ );
+ }
+ else
+ {
+ MessageList = new ObservableCollection();
+ }
+ }
+ else
+ {
+ MessageList = new ObservableCollection();
+ }
+
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private int _collectionInterval = 1000;
+ public int CollectionInterval
+ {
+ get => _collectionInterval;
+ set => SetProperty(ref _collectionInterval, value);
+ }
+ #endregion
+
+ #region 命令
+ public ICommand AddCommand { get; set; }
+ public ICommand CloseCommand { get; set; }
+ public ICommand SaveCommand { get; set; }
+ public ICommand DeleteCommand { get; set; }
+ #endregion
+
+ public CollectCANSignalSettingViewModel(IContainerProvider containerProvider) : base(containerProvider)
+ {
+ CloseCommand = new DelegateCommand(Close);
+ AddCommand = new DelegateCommand(Add);
+ SaveCommand = new DelegateCommand(Save);
+ DeleteCommand = new DelegateCommand(Delete);
+ }
+
+ #region Dialog 生命周期
+ public override void OnDialogOpened(IDialogParameters parameters)
+ {
+ _systemConfig = parameters.GetValue("SystemConfig");
+ _canfd = _systemConfig?.CANFD;
+ string title = _systemConfig.Title;
+ char lastChar = title[title.Length - 1];
+ // 3. "TestCellX"转int
+ MessageChannel = int.Parse(lastChar.ToString())-1;
+ if (MessageChannel >= 4) MessageChannel -= 4;
+ RaisePropertyChanged(nameof(ConfigurationList));
+ }
+ #endregion
+
+ #region 命令处理
+ private void Delete()
+ {
+ if (SelectedItem != null)
+ {
+ _systemConfig?.ConfigurationList.Remove(SelectedItem);
+ }
+ }
+
+ private void Save()
+ {
+ ConfigService.Save(_systemConfig);
+ }
+
+
+ private void Add()
+ {
+ if (string.IsNullOrEmpty(SelectedSignal) || string.IsNullOrEmpty(SelectedMessage) || CollectionInterval < 100)
+ {
+ return;
+ }
+ if (_systemConfig == null) return;
+
+ string hexString = ExtractHex(SelectedMessage);
+ if (string.IsNullOrEmpty(hexString)) return;
+
+ int decimalValue = ConvertHexToDecimal(hexString);
+ if (_systemConfig.ConfigurationList.Any(x => x.SignalName == SelectedSignal && x.Channel == MessageChannel))
+ {
+ return;
+ }
+
+ _systemConfig.ConfigurationList.Add(new CANSignalConfig
+ {
+ CollectionID = Guid.NewGuid(),
+ Channel = MessageChannel,
+ MethodName = CANSignalBroadcaster.BuildMethodName((uint)decimalValue, SelectedSignal),
+ MessageName = ExtractMessageName(SelectedMessage),
+ SignalName = SelectedSignal,
+ MessageID = decimalValue,
+ CollectionInterval = CollectionInterval
+ });
+ }
+
+ private string ExtractHex(string input)
+ {
+ int startIndex = input.IndexOf("0x") + 2;
+ int endIndex = input.IndexOf("]", startIndex);
+ if (startIndex >= 2 && endIndex > startIndex)
+ {
+ return input.Substring(startIndex, endIndex - startIndex);
+ }
+ return string.Empty;
+ }
+
+ private int ConvertHexToDecimal(string hex)
+ {
+ try
+ {
+ return Convert.ToInt32(hex, 16);
+ }
+ catch (FormatException)
+ {
+ return -1;
+ }
+ }
+
+ private string ExtractMessageName(string input)
+ {
+ var parts = input.Split(']');
+ return parts.Length > 1 ? parts[1] : string.Empty;
+ }
+
+ private void Close()
+ {
+ RequestClose.Invoke();
+ }
+ #endregion
+ }
+}
diff --git a/CANModule/ViewModels/SelectCanMessageViewModel.cs b/CANModule/ViewModels/SelectCanMessageViewModel.cs
new file mode 100644
index 0000000..3b06b12
--- /dev/null
+++ b/CANModule/ViewModels/SelectCanMessageViewModel.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Input;
+using UIShare.ViewModelBase;
+using ZLGUSBCANFD;
+
+namespace CANModule.ViewModels
+{
+ public class SelectCanMessageViewModel : DialogViewModelBase
+ {
+ #region 字段
+ private ZLGCANFD? _canfd;
+ #endregion
+
+ #region 属性
+ public DialogCloseListener RequestClose { get; set; }
+
+ private ObservableCollection _ChannelList;
+ public ObservableCollection ChannelList
+ {
+ get => _ChannelList;
+ set => SetProperty(ref _ChannelList, value);
+ }
+
+ private ObservableCollection _messageList;
+ public ObservableCollection MessageList
+ {
+ get => _messageList;
+ set => SetProperty(ref _messageList, value);
+ }
+
+ private ObservableCollection _signalList;
+ public ObservableCollection SignalList
+ {
+ get => _signalList;
+ set => SetProperty(ref _signalList, value);
+ }
+
+ private string _title = "CollectCANSignalSetting";
+ public string Title
+ {
+ get => _title;
+ set => SetProperty(ref _title, value);
+ }
+
+ private string _selectedMessage;
+ public string SelectedMessage
+ {
+ get => _selectedMessage;
+ set
+ {
+ if (SetProperty(ref _selectedMessage, value))
+ {
+ if (!string.IsNullOrEmpty(value) && _canfd != null)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (MessageChannel >= 0 && MessageChannel < db.Count)
+ {
+ var targetMsg = db[MessageChannel]
+ .FirstOrDefault(x => $"[0x{x.msg_id:X}]{x.msg_name}" == value);
+
+ if (targetMsg != null && targetMsg.signal_Name != null)
+ {
+ SignalList = new ObservableCollection(targetMsg.signal_Name);
+ }
+ else
+ {
+ SignalList = new ObservableCollection();
+ }
+ }
+ }
+ else
+ {
+ SignalList = new ObservableCollection();
+ }
+ }
+ }
+ }
+
+ private string _selectedSignal;
+ public string SelectedSignal
+ {
+ get => _selectedSignal;
+ set => SetProperty(ref _selectedSignal, value);
+ }
+
+ private int _MessageChannel = -1;
+ public int MessageChannel
+ {
+ get => _MessageChannel;
+ set
+ {
+ if (_MessageChannel != value)
+ {
+ _MessageChannel = value;
+
+ // 1. 先安全清空旧的选择项和信号列表
+ SelectedMessage = null;
+ SelectedSignal = null;
+ SignalList = new ObservableCollection();
+
+ // 2. 刷新当前通道的报文列表
+ if (_canfd != null)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (db != null && value >= 0 && value < db.Count)
+ {
+ MessageList = new ObservableCollection(
+ db[value].Select(s => $"[0x{s.msg_id:X}]{s.msg_name}")
+ );
+ }
+ else
+ {
+ MessageList = new ObservableCollection();
+ }
+ }
+ else
+ {
+ MessageList = new ObservableCollection();
+ }
+
+ RaisePropertyChanged();
+ }
+ }
+ }
+ #endregion
+
+ #region 命令
+ public ICommand CloseCommand { get; set; }
+ public ICommand GetSelectedMessageCommand { get; set; }
+ public ICommand GetSelectedSignalCommand { get; set; }
+ public ICommand LoadCommand { get; set; }
+ #endregion
+
+ public SelectCanMessageViewModel(IContainerProvider containerProvider) : base(containerProvider)
+ {
+ CloseCommand = new DelegateCommand(Close);
+ GetSelectedMessageCommand = new DelegateCommand(GetSelectedMessage);
+ GetSelectedSignalCommand = new DelegateCommand(GetSelectedSignal);
+ LoadCommand = new DelegateCommand(Load);
+ }
+
+ #region Dialog 生命周期
+ public override void OnDialogOpened(IDialogParameters parameters)
+ {
+ _canfd = parameters.GetValue("CANFD");
+ }
+ #endregion
+
+ #region 命令处理
+ private void Close()
+ {
+ RequestClose.Invoke();
+ }
+
+ private void Load()
+ {
+ int maxCh = _canfd?.DBCParser.MaxChannels ?? 4;
+ ChannelList = new ObservableCollection(Enumerable.Range(0, maxCh));
+ }
+
+ private void GetSelectedSignal()
+ {
+ if (!string.IsNullOrEmpty(SelectedSignal))
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Clipboard.SetDataObject(SelectedSignal);
+ });
+ }
+ }
+
+ private void GetSelectedMessage()
+ {
+ if (!string.IsNullOrEmpty(SelectedMessage) && SelectedMessage.Contains(']'))
+ {
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ Clipboard.SetDataObject(SelectedMessage);
+ });
+ }
+ }
+ #endregion
+ }
+}
diff --git a/CANModule/ViewModels/ZLGCANFDViewModel.cs b/CANModule/ViewModels/ZLGCANFDViewModel.cs
new file mode 100644
index 0000000..1ebc85e
--- /dev/null
+++ b/CANModule/ViewModels/ZLGCANFDViewModel.cs
@@ -0,0 +1,649 @@
+using Microsoft.Win32;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using UIShare.GlobalVariable;
+using UIShare.UIViewModel;
+using UIShare.ViewModelBase;
+using ZLGUSBCANFD;
+
+namespace CANModule.ViewModels
+{
+ public class ZLGCANFDViewModel : NavigateViewModelBase
+ {
+ #region 字段
+ private ZLGCANFD? _canfd;
+ private SystemConfig? _systemConfig;
+ private CancellationTokenSource? _refreshCts;
+ private readonly DeviceManager _deviceManager;
+ #endregion
+
+ #region 属性
+ private string _Title = "周立功CAN";
+ public string Title
+ {
+ get => _Title;
+ set => SetProperty(ref _Title, value);
+ }
+
+ private ObservableCollection _CanMessageList = new();
+ public ObservableCollection CanMessageList
+ {
+ get => _CanMessageList;
+ set => SetProperty(ref _CanMessageList, value);
+ }
+
+ private int _DBCChannel = 0;
+ public int DBCChannel
+ {
+ get => _DBCChannel;
+ set => SetProperty(ref _DBCChannel, value);
+ }
+
+ private bool _IsDirectlySend = false;
+ public bool IsDirectlySend
+ {
+ get => _IsDirectlySend;
+ set => SetProperty(ref _IsDirectlySend, value);
+ }
+
+ private ObservableCollection _Message;
+ public ObservableCollection Message
+ {
+ get => _Message;
+ set => SetProperty(ref _Message, value);
+ }
+
+ private ObservableCollection _Signal;
+ public ObservableCollection Signal
+ {
+ get => _Signal;
+ set => SetProperty(ref _Signal, value);
+ }
+
+ private byte _channel;
+ private string _selectedMessage;
+ public string SelectedMessage
+ {
+ get => _selectedMessage;
+ set
+ {
+ if (_selectedMessage != value)
+ {
+ if (!string.IsNullOrEmpty(value) && _canfd != null)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (_channel >= 0 && _channel < db.Count)
+ {
+ var targetMsg = db[(int)_channel]
+ .FirstOrDefault(x => $"[0x{x.msg_id:X}]{x.msg_name}" == value);
+
+ if (targetMsg != null && targetMsg.signal_Name != null)
+ {
+ Signal = new ObservableCollection(targetMsg.signal_Name);
+ }
+ else
+ {
+ Signal = new ObservableCollection();
+ }
+ }
+ }
+ else
+ {
+ Signal = new ObservableCollection();
+ }
+ _selectedMessage = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private string _selectedSignal;
+ public string SelectedSignal
+ {
+ get => _selectedSignal;
+ set => SetProperty(ref _selectedSignal, value);
+ }
+
+ private string _signalValue;
+ public string SignalValue
+ {
+ get => _signalValue;
+ set => SetProperty(ref _signalValue, value);
+ }
+
+ private string _cycleSendInterval;
+ public string CycleSendInterval
+ {
+ get => _cycleSendInterval;
+ set => SetProperty(ref _cycleSendInterval, value);
+ }
+
+ private int _messageSendChannel = 0;
+ public int MessageSendChannel
+ {
+ get => _messageSendChannel;
+ set => SetProperty(ref _messageSendChannel, value);
+ }
+
+ public string BLFPath
+ {
+ get => _systemConfig?.DefaultBLFFilePath ?? "";
+ set
+ {
+ if (_systemConfig != null && _systemConfig.DefaultBLFFilePath != value)
+ {
+ _systemConfig.DefaultBLFFilePath = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ public string DBCPath
+ {
+ get => _systemConfig?.DefaultDBCFilePath ?? "";
+ set
+ {
+ if (_systemConfig != null && _systemConfig.DefaultDBCFilePath != value)
+ {
+ _systemConfig.DefaultDBCFilePath = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private bool _IsSaved;
+ public bool IsSaved
+ {
+ get => _IsSaved;
+ set => SetProperty(ref _IsSaved, value);
+ }
+
+ public ObservableCollection DBCAutoLoadList
+ {
+ get => _systemConfig?.DBCAutoLoadList ?? new ObservableCollection();
+ }
+
+ private AutoDBCLoadItem _SelectedDcAutoLoad;
+ public AutoDBCLoadItem SelectedDcAutoLoad
+ {
+ get => _SelectedDcAutoLoad;
+ set => SetProperty(ref _SelectedDcAutoLoad, value);
+ }
+
+ // ===== 自定义报文发送属性 =====
+ private APP_CHANNEL[] _appChannelNames = Enum.GetValues();
+ public APP_CHANNEL[] APP_CHANNEL
+ {
+ get => _appChannelNames;
+ set => SetProperty(ref _appChannelNames, value);
+ }
+
+ private APP_CHANNEL _aIdxChn;
+ public APP_CHANNEL AIdxChn
+ {
+ get => _aIdxChn;
+ set => SetProperty(ref _aIdxChn, value);
+ }
+
+ private int _aID;
+ public int AID
+ {
+ get => _aID;
+ set => SetProperty(ref _aID, value);
+ }
+
+ private bool _aIsTx;
+ public bool AIsTx
+ {
+ get => _aIsTx;
+ set => SetProperty(ref _aIsTx, value);
+ }
+
+ private bool _aIsExt;
+ public bool AIsExt
+ {
+ get => _aIsExt;
+ set => SetProperty(ref _aIsExt, value);
+ }
+
+ private bool _aIsRemote;
+ public bool AIsRemote
+ {
+ get => _aIsRemote;
+ set => SetProperty(ref _aIsRemote, value);
+ }
+
+ private byte _aDlc;
+ public byte ADLC
+ {
+ get => _aDlc;
+ set => SetProperty(ref _aDlc, value);
+ }
+
+ private byte[] _aDataArray;
+ public byte[] ADataArray
+ {
+ get => _aDataArray;
+ set => SetProperty(ref _aDataArray, value);
+ }
+
+ private bool _aIsFD = true;
+ public bool AIsFD
+ {
+ get => _aIsFD;
+ set => SetProperty(ref _aIsFD, value);
+ }
+
+ private bool _aIsBRS = false;
+ public bool AIsBRS
+ {
+ get => _aIsBRS;
+ set => SetProperty(ref _aIsBRS, value);
+ }
+
+ private bool _AutoSetting = true;
+ public bool AutoSetting
+ {
+ get => _AutoSetting;
+ set => SetProperty(ref _AutoSetting, value);
+ }
+
+ private string _CustomMessage = "[00,11,22,33,AA,BB,CC,DD]";
+ public string CustomMessage
+ {
+ get => _CustomMessage;
+ set => SetProperty(ref _CustomMessage, value);
+ }
+
+ private float _CustomSendCycleInterval = 0.0f;
+ public float CustomSendCycleInterval
+ {
+ get => _CustomSendCycleInterval;
+ set => SetProperty(ref _CustomSendCycleInterval, value);
+ }
+
+ //private bool _UpdateToDB;
+ //public bool UpdateToDB
+ //{
+ // get => _UpdateToDB;
+ // set => SetProperty(ref _UpdateToDB, value);
+ //}
+
+ // ===== 连接状态 =====
+ private bool _IsDeviceOpened;
+ public bool IsDeviceOpened
+ {
+ get => _IsDeviceOpened;
+ set => SetProperty(ref _IsDeviceOpened, value);
+ }
+
+ private ObservableCollection _ChannelList;
+ public ObservableCollection ChannelList
+ {
+ get => _ChannelList;
+ set => SetProperty(ref _ChannelList, value);
+ }
+
+ private int _SelectedChannel = -1;
+ public int SelectedChannel
+ {
+ get => _SelectedChannel;
+ set
+ {
+ if (_SelectedChannel != value)
+ {
+ _SelectedChannel = value;
+ if (value >= 0 && _canfd != null)
+ {
+ _canfd.初始化并启动通道((uint)value);
+ }
+ RaisePropertyChanged();
+ }
+ }
+ }
+ #endregion
+
+ #region 命令
+ public ICommand SelectBLFPathCommand { get; set; }
+ public ICommand SaveBLFPathCommand { get; set; }
+ public ICommand StartTranscribeCommand { get; set; }
+ public ICommand EndTranscribeCommand { get; set; }
+ public ICommand ConnectCanCommand { get; set; }
+ public ICommand CloseCanCommand { get; set; }
+ public ICommand ClearCycleCommand { get; set; }
+ public ICommand UpLoadDBCCommand { get; set; }
+ public ICommand ChangeDBCCommand { get; set; }
+ public ICommand LoadDBCCommand { get; set; }
+ public ICommand LoadMessageCommand { get; set; }
+ public ICommand SetAndSendMessageCommand { get; set; }
+ public ICommand DeleteDcAutoLoadCommand { get; set; }
+ public ICommand SendCustomMessageCommand { get; set; }
+ public ICommand LoadCommand { get; set; }
+ #endregion
+
+ public ZLGCANFDViewModel(IContainerProvider containerProvider) : base(containerProvider)
+ {
+ _systemConfig = containerProvider.Resolve();
+ _deviceManager = containerProvider.Resolve();
+ _canfd = _deviceManager.CANFD;
+ SelectBLFPathCommand = new DelegateCommand(SelectBLFPath);
+ SaveBLFPathCommand = new DelegateCommand(SaveBLFPath);
+ StartTranscribeCommand = new DelegateCommand(StartTranscribe);
+ EndTranscribeCommand = new DelegateCommand(EndTranscribe);
+ ConnectCanCommand = new DelegateCommand(ConnectCan);
+ CloseCanCommand = new DelegateCommand(CloseCan);
+ ClearCycleCommand = new DelegateCommand(ClearCycle);
+ UpLoadDBCCommand = new DelegateCommand(UpLoadDBC);
+ ChangeDBCCommand = new DelegateCommand(ChangeDBC);
+ LoadDBCCommand = new DelegateCommand(LoadDBC);
+ SetAndSendMessageCommand = new DelegateCommand(SetAndSendMessage);
+ LoadMessageCommand = new DelegateCommand(LoadMessage);
+ DeleteDcAutoLoadCommand = new DelegateCommand(DeleteDcAutoLoad);
+ SendCustomMessageCommand = new DelegateCommand(SendCustomMessage);
+ _canfd.OnDbcMessageDecoded += OnDbcMessageDecoded;
+ LoadCommand = new DelegateCommand(Load);
+ }
+
+ #region Dialog 生命周期
+ public override void OnNavigatedTo(NavigationContext navigationContext)
+ {
+ if (_canfd != null)
+ {
+
+ }
+
+ RaisePropertyChanged(nameof(BLFPath));
+ RaisePropertyChanged(nameof(DBCPath));
+ RaisePropertyChanged(nameof(DBCAutoLoadList));
+ }
+
+ public override void OnNavigatedFrom(NavigationContext navigationContext)
+ {
+ if (_canfd != null)
+ {
+ _canfd.OnDbcMessageDecoded -= OnDbcMessageDecoded;
+ }
+ _refreshCts?.Cancel();
+ }
+ #endregion
+
+ #region 命令处理
+ private void Load()
+ {
+ int maxCh = _canfd?.DBCParser.MaxChannels ?? 4;
+ ChannelList = new ObservableCollection(Enumerable.Range(0, maxCh));
+ }
+
+ private void SendCustomMessage()
+ {
+ if (_canfd == null) return;
+
+ try
+ {
+ ADataArray = 报文转换(CustomMessage);
+ if (ADataArray.Length > byte.MaxValue) throw new Exception("报文数量超过上限!");
+ if (AutoSetting)
+ {
+ ADLC = (byte)ADataArray.Length;
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowErrorMessageBox($"报文转换失败:{ex.Message}", () => { });
+ return;
+ }
+
+ _canfd.发送自定义报文(
+ (uint)AIdxChn,
+ AID.ToString(),
+ ADataArray,
+ ADLC,
+ AIsExt,
+ AIsFD,
+ AIsBRS);
+ }
+
+ private void DeleteDcAutoLoad()
+ {
+ if (SelectedDcAutoLoad != null)
+ {
+ DBCAutoLoadList.Remove(SelectedDcAutoLoad);
+ }
+ }
+
+ private void LoadMessage()
+ {
+ if (_canfd == null) return;
+
+ if (MessageSendChannel >= 0)
+ {
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (MessageSendChannel < db.Count)
+ {
+ Message = new ObservableCollection(
+ db[MessageSendChannel].Select(s => $"[0x{s.msg_id:X}]{s.msg_name}"));
+ }
+ }
+ }
+
+ private void SetAndSendMessage()
+ {
+ if (_canfd == null) return;
+
+ if (int.TryParse(CycleSendInterval, out int sendPeriod) &&
+ double.TryParse(SignalValue, out double SValue) &&
+ MessageSendChannel >= 0)
+ {
+ _channel = (byte)MessageSendChannel;
+ var message = SelectedMessage.Split(']')[1];
+
+ // 通过 DBC 查找帧 ID
+ var db = _canfd.DBCParser.MsgDatabase;
+ if (MessageSendChannel < db.Count)
+ {
+ var targetMsg = db[MessageSendChannel]
+ .FirstOrDefault(x => $"[0x{x.msg_id:X}]{x.msg_name}" == SelectedMessage);
+
+ if (targetMsg != null)
+ {
+ _canfd.设置报文((uint)MessageSendChannel, targetMsg.msg_id.ToString(), SelectedSignal, SValue, sendPeriod, IsDirectlySend);
+ }
+ }
+ }
+ }
+
+ private void UpLoadDBC()
+ {
+ if (_canfd == null) return;
+
+ // 释放当前通道的 DBC
+ if (DBCChannel >= 0)
+ {
+ _canfd.释放通道DBC((uint)DBCChannel);
+ }
+ }
+
+ private void ChangeDBC()
+ {
+ var dialog = new OpenFileDialog
+ {
+ Title = "更换DBC文件",
+ Filter = "DBC 文件 (*.dbc)|*.dbc|所有文件 (*.*)|*.*",
+ DefaultExt = ".dbc",
+ };
+ if (dialog.ShowDialog() == true)
+ {
+ DBCPath = System.IO.Path.GetFullPath(dialog.FileName);
+ }
+ }
+
+ private void LoadDBC()
+ {
+ if (_canfd == null || DBCChannel < 0) return;
+
+ bool re = _canfd.加载通道DBC文件((uint)DBCChannel, DBCPath);
+
+ if (re && IsSaved)
+ {
+ var item = DBCAutoLoadList.FirstOrDefault(s => s.DBCChannel == DBCChannel);
+ if (item == null)
+ {
+ DBCAutoLoadList.Add(new AutoDBCLoadItem
+ {
+ DBCFilePath = DBCPath,
+ DBCChannel = DBCChannel
+ });
+ }
+ else
+ {
+ DBCAutoLoadList.Remove(item);
+ DBCAutoLoadList.Add(new AutoDBCLoadItem
+ {
+ DBCFilePath = DBCPath,
+ DBCChannel = DBCChannel
+ });
+ }
+ ConfigService.Save(_systemConfig);
+ }
+ }
+ private void ClearCycle()
+ {
+ _canfd?.停止所有循环发送();
+ }
+
+ private void CloseCan()
+ {
+ if (_canfd == null) return;
+
+ _canfd.关闭CAN卡设备();
+ IsDeviceOpened = false;
+ _refreshCts?.Cancel();
+ }
+
+ private void ConnectCan()
+ {
+ if (_canfd == null) return;
+
+ bool re = _canfd.打开设备();
+ IsDeviceOpened = re;
+
+ if (re)
+ {
+ // 启动实时报文刷新
+ _refreshCts?.Cancel();
+ _refreshCts = new CancellationTokenSource();
+ _ = Refresh(_refreshCts.Token);
+ }
+ }
+
+ private void EndTranscribe()
+ {
+ // TODO: BLF 录制功能在 ZLGCANFD 中未实现,暂不支持
+ ShowInfoMessageBox("BLF 录制功能尚未在此硬件驱动中实现。", () => { });
+ }
+
+ private void StartTranscribe()
+ {
+ // TODO: BLF 录制功能在 ZLGCANFD 中未实现,暂不支持
+ ShowInfoMessageBox("BLF 录制功能尚未在此硬件驱动中实现。", () => { });
+ }
+
+ private void SaveBLFPath()
+ {
+ // 路径已直接绑定到 SystemConfig,无需额外操作
+ }
+
+ private void SelectBLFPath()
+ {
+ var dialog = new SaveFileDialog
+ {
+ Title = "保存 CAN报文回放 BLF 文件",
+ Filter = "BLF 文件 (*.blf)|*.blf|所有文件 (*.*)|*.*",
+ DefaultExt = ".blf",
+ FileName = "log.blf",
+ OverwritePrompt = true
+ };
+ if (dialog.ShowDialog() == true)
+ {
+ BLFPath = System.IO.Path.GetFullPath(dialog.FileName);
+ }
+ }
+
+ #endregion
+
+ #region 辅助方法
+ public byte[] 报文转换(string s)
+ {
+ s = s.Replace("[", string.Empty).Replace("]", string.Empty);
+ var strs = s.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ byte[] bytes = new byte[strs.Length];
+ for (int i = 0; i < strs.Length; i++)
+ {
+ bytes[i] = Convert.ToByte(strs[i], 16);
+ }
+ return bytes;
+ }
+
+ private async Task Refresh(CancellationToken ct = default)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ // 通过 OnDbcMessageDecoded 事件已实时更新 CanMessageList
+ // 此处仅做周期性 UI 刷新触发
+ await Task.Delay(50, ct);
+ }
+ }
+
+ private void OnDbcMessageDecoded(uint channel, ZDBC.DBCMessage msg)
+ {
+ // DBC 解码后的报文回调
+ Application.Current.Dispatcher.BeginInvoke(() =>
+ {
+ var msgName = System.Text.Encoding.Default.GetString(msg.strName).TrimEnd('\0');
+ var find = CanMessageList.FirstOrDefault(s => s.报文ID == (int)msg.nID&& s.通道==channel);
+ if (find != null)
+ {
+ find.通道 = (byte)channel;
+ find.报文ID = (int)msg.nID;
+ find.时间戳 = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds(); ;
+ find.长度 = (byte)msg.nSize;
+ }
+ else
+ {
+ CanMessageList.Add(new CanMessageShowVM
+ {
+ 通道 = (byte)channel,
+ 报文ID = (int)msg.nID,
+ 时间戳 = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds(),
+ 长度 = (byte)msg.nSize
+ });
+ }
+ });
+ }
+
+ private string 十六进制字符串转换(System.Span s)
+ {
+ if (s == null || s.Length == 0)
+ return "[]";
+
+ var sb = new StringBuilder();
+ sb.Append('[');
+ for (int i = 0; i < s.Length; i++)
+ {
+ sb.Append("0x");
+ sb.Append(s[i].ToString("X2"));
+ if (i < s.Length - 1)
+ sb.Append(", ");
+ }
+ sb.Append(']');
+ return sb.ToString();
+ }
+ #endregion
+ }
+}
diff --git a/CANModule/Views/CollectCANSignalSettingView.xaml b/CANModule/Views/CollectCANSignalSettingView.xaml
new file mode 100644
index 0000000..dfb3fa9
--- /dev/null
+++ b/CANModule/Views/CollectCANSignalSettingView.xaml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CANModule/Views/CollectCANSignalSettingView.xaml.cs b/CANModule/Views/CollectCANSignalSettingView.xaml.cs
new file mode 100644
index 0000000..5bd739e
--- /dev/null
+++ b/CANModule/Views/CollectCANSignalSettingView.xaml.cs
@@ -0,0 +1,25 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace CANModule.Views
+{
+ ///
+ /// CollectCANSignalSettingView.xaml 的交互逻辑
+ ///
+ public partial class CollectCANSignalSettingView : UserControl
+ {
+ public CollectCANSignalSettingView()
+ {
+ InitializeComponent();
+ }
+
+ private void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ Window.GetWindow(this)?.DragMove();
+ }
+ }
+ }
+}
diff --git a/CANModule/Views/SelectCanMessageView.xaml b/CANModule/Views/SelectCanMessageView.xaml
new file mode 100644
index 0000000..9edee97
--- /dev/null
+++ b/CANModule/Views/SelectCanMessageView.xaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CANModule/Views/SelectCanMessageView.xaml.cs b/CANModule/Views/SelectCanMessageView.xaml.cs
new file mode 100644
index 0000000..053f0c0
--- /dev/null
+++ b/CANModule/Views/SelectCanMessageView.xaml.cs
@@ -0,0 +1,36 @@
+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 CANModule.Views
+{
+ ///
+ /// SelectCanMessageView.xaml 的交互逻辑
+ ///
+ public partial class SelectCanMessageView : UserControl
+ {
+ public SelectCanMessageView()
+ {
+ InitializeComponent();
+ }
+
+ private void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ Window.GetWindow(this)?.DragMove();
+ }
+ }
+ }
+}
diff --git a/CANModule/Views/ZLGCANFDView.xaml b/CANModule/Views/ZLGCANFDView.xaml
new file mode 100644
index 0000000..b063f7c
--- /dev/null
+++ b/CANModule/Views/ZLGCANFDView.xaml
@@ -0,0 +1,286 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CANModule/Views/ZLGCANFDView.xaml.cs b/CANModule/Views/ZLGCANFDView.xaml.cs
new file mode 100644
index 0000000..2e4e7f6
--- /dev/null
+++ b/CANModule/Views/ZLGCANFDView.xaml.cs
@@ -0,0 +1,35 @@
+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 CANModule.Views
+{
+ ///
+ /// ZLGCANFDView.xaml 的交互逻辑
+ ///
+ public partial class ZLGCANFDView : UserControl
+ {
+ public ZLGCANFDView()
+ {
+ InitializeComponent();
+ }
+ private void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ {
+ Window.GetWindow(this)?.DragMove();
+ }
+ }
+ }
+}
diff --git a/Command/Command.csproj b/Command/Command.csproj
new file mode 100644
index 0000000..0eabc2b
--- /dev/null
+++ b/Command/Command.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Command/CommandApplication.cs b/Command/CommandApplication.cs
new file mode 100644
index 0000000..6b44e19
--- /dev/null
+++ b/Command/CommandApplication.cs
@@ -0,0 +1,268 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+
+ [ACPCommand]
+ public static class CommandApplication
+ {
+ ///
+ /// 打开外部应用程序
+ ///
+ /// 程序路径
+ /// 是否等待关闭
+ /// 是否请求管理员运行
+ public static void OpenApplication(string route, bool waitShutdown, bool AdminRun)
+ {
+
+ // 创建一个新的进程实例
+ Process process = new Process();
+
+ try
+ {
+ // 指定要启动的程序路径
+ process.StartInfo.FileName = route;
+
+ // 指定是否等待程序关闭
+ process.StartInfo.UseShellExecute = !waitShutdown;
+ if (AdminRun) process.StartInfo.Verb = "runas";
+ // 启动程序
+ process.Start();
+
+ // 如果需要等待程序关闭,则等待程序退出
+ if (waitShutdown)
+ {
+ process.WaitForExit();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("错误!: " + ex.Message);
+ }
+ finally
+ {
+ // 确保进程对象被释放
+ process.Dispose();
+ }
+
+ }
+ ///
+ /// 使用默认方式打开一个文件
+ ///
+ /// 文件路径
+ public static void OpenFile(string route)
+ {
+ // 创建一个新的进程实例
+ Process process = new Process();
+
+ try
+ {
+ // 指定要启动的程序路径
+ process.StartInfo.FileName = route;
+ process.StartInfo.UseShellExecute = true;
+ // 启动程序
+ process.Start();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("错误!: " + ex.Message);
+ }
+ finally
+ {
+ // 确保进程对象被释放
+ process.Dispose();
+ }
+
+ }
+
+
+
+ // Python引擎相关字段
+ private static Process _pythonProcess;
+ private static string _pythonPath = string.Empty;
+ private static bool _isPythonEngineInitialized = false;
+
+ ///
+ /// 初始化python引擎
+ ///
+ /// Python解释器路径
+ public static void Initialization_Python(string py路径)
+ {
+ try
+ {
+ // 验证Python路径是否存在
+ if (string.IsNullOrEmpty(py路径) || !File.Exists(py路径))
+ {
+ throw new FileNotFoundException($"Python解释器路径不存在: {py路径}");
+ }
+
+ // 验证是否为Python可执行文件
+ var fileInfo = new FileInfo(py路径);
+ if (!fileInfo.Name.ToLower().Contains("python"))
+ {
+ Console.WriteLine("警告: 指定的文件可能不是Python解释器");
+ }
+
+ // 测试Python是否能正常运行
+ var testProcess = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = py路径,
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ }
+ };
+
+ testProcess.Start();
+ testProcess.WaitForExit(5000); // 5秒超时
+
+ if (testProcess.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"Python解释器验证失败,退出码: {testProcess.ExitCode}");
+ }
+
+ _pythonPath = py路径;
+ _isPythonEngineInitialized = true;
+
+ Console.WriteLine($"Python引擎初始化成功,版本: {testProcess.StandardOutput.ReadToEnd().Trim()}");
+ }
+ catch (Exception ex)
+ {
+ _isPythonEngineInitialized = false;
+ _pythonPath = string.Empty;
+ Console.WriteLine($"初始化Python引擎失败: {ex.Message}");
+ throw;
+ }
+ }
+
+ ///
+ /// 运行python脚本
+ ///
+ /// Python脚本路径
+ /// 脚本执行结果
+ public static string Run_Python_Script(string 路径)
+ {
+ if (!_isPythonEngineInitialized)
+ {
+ throw new InvalidOperationException("Python引擎未初始化,请先调用初始化python引擎");
+ }
+
+ if (string.IsNullOrEmpty(路径) || !File.Exists(路径))
+ {
+ throw new FileNotFoundException($"Python脚本路径不存在: {路径}");
+ }
+
+ Process process = null;
+ try
+ {
+ process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = _pythonPath,
+ Arguments = $"\"{路径}\"",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ }
+ };
+
+ var output = new StringBuilder();
+ var error = new StringBuilder();
+
+ // 设置输出和错误处理
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ output.AppendLine(e.Data);
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ error.AppendLine(e.Data);
+ };
+
+ process.Start();
+
+ // 异步读取输出
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ process.WaitForExit();
+
+ var outputResult = output.ToString().Trim();
+ var errorResult = error.ToString().Trim();
+
+ if (process.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"Python脚本执行失败,退出码: {process.ExitCode}, 错误: {errorResult}");
+ }
+
+ return string.IsNullOrEmpty(outputResult) ? "执行成功" : outputResult;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"运行Python脚本失败: {ex.Message}");
+ throw;
+ }
+ finally
+ {
+ process?.Dispose();
+ }
+ }
+
+ ///
+ /// 释放python引擎
+ ///
+ /// Python解释器路径(可选,用于验证)
+ public static void Release_Python(string py = "")
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(py) && py != _pythonPath)
+ {
+ Console.WriteLine($"警告: 释放的Python路径与当前初始化路径不匹配");
+ }
+
+ // 如果有正在运行的Python进程,尝试终止它
+ if (_pythonProcess != null && !_pythonProcess.HasExited)
+ {
+ try
+ {
+ _pythonProcess.Kill();
+ _pythonProcess.WaitForExit(1000);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"终止Python进程时出错: {ex.Message}");
+ }
+ }
+
+ _pythonPath = string.Empty;
+ _isPythonEngineInitialized = false;
+ _pythonProcess = null;
+
+ Console.WriteLine("Python引擎已释放");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"释放Python引擎失败: {ex.Message}");
+ throw;
+ }
+ }
+ }
+}
diff --git a/Command/CommandArray.cs b/Command/CommandArray.cs
new file mode 100644
index 0000000..d142675
--- /dev/null
+++ b/Command/CommandArray.cs
@@ -0,0 +1,261 @@
+using Common.Attributes;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+
+ [ACPCommand]
+ public static class CommandArray
+ {
+ #region 数组操作
+ ///
+ /// 从数组中获取指定位置的元素。
+ ///
+ /// 要索引的数组。
+ /// 要获取的元素的位置。
+ /// 指定位置的元素。
+ public static object IndexArray(object Array, int Index)
+ {
+ return ((dynamic)Array)[Index];
+ }
+
+ ///
+ /// 设置数组中指定位置的元素为给定值。
+ ///
+ /// 要设置元素的数组。
+ /// 要设置的元素的位置。
+ /// 要设置的值。
+ /// 修改后的数组。
+ public static object SetArrayElement(object Array, int Location, object Value)
+ {
+ ((dynamic)Array)[Location] = (dynamic)Value;
+ return Array;
+ }
+ ///
+ /// 将给定数组的长度设置为指定值,并返回一个新的数组。
+ /// 如果指定的长度小于原始数组的长度,则截取原始数组的一部分作为新数组。
+ ///
+ /// 要设置长度的数组。
+ /// 新数组的长度。
+ /// 新的数组,或者原始数组(如果输入参数不是数组类型)。
+ public static object SetArrayLength(object Arr, int Length)
+ {
+ if (Arr is Array)
+ {
+ // 创建一个新的数组实例来存储结果
+ Array temp = Array.CreateInstance(Arr.GetType()!.GetElementType()!, Length);
+ // 确定复制的长度,取原始数组长度和目标长度的最小值
+ int CopyLength = Math.Min(((Array)Arr).Length, Length);
+ // 复制原始数组的元素到新数组中
+ Array.Copy((Array)Arr, temp, CopyLength);
+ return temp;
+ }
+ else
+ {
+ // 如果输入参数不是数组类型,则返回原始数组
+ return Arr;
+ }
+ }
+
+ ///
+ /// 在指定位置插入元素到数组中。
+ ///
+ /// 要插入元素的数组。
+ /// 要插入元素的位置。
+ /// 要插入的元素。
+ /// 插入元素后的数组。
+ public static object SetArrayInsertElement(object Arr, int Location, object InsertElement)
+ {
+ // 将数组转换为可编辑列表
+ var temp = Enumerable.ToList((dynamic)Arr);
+
+ // 在指定位置插入元素
+ temp.Insert(Location, (dynamic)InsertElement);
+
+ // 如果原数组是数组类型,则将可编辑列表转换回数组并返回
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+
+ // 返回插入元素后的可编辑列表
+ return temp;
+ }
+ ///
+ /// 从数组中删除指定的元素。
+ ///
+ /// 要删除元素的数组。
+ /// 要从数组中删除的元素。
+ /// 删除元素后的数组。
+ public static object SetArrayDeleteElement(object Arr, object DeleteElement)
+ {
+ // 将数组转换为可编辑列表
+ var temp = Enumerable.ToList((dynamic)Arr);
+
+ // 从列表中移除指定元素
+ temp.Remove((dynamic)DeleteElement);
+
+ // 如果原数组是数组类型,则将可编辑列表转换回数组并返回
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+
+ // 返回删除元素后的可编辑列表
+ return temp;
+ }
+ ///
+ /// 从数组中删除指定位置的元素。
+ ///
+ /// 要删除元素的数组。
+ /// 要从数组中删除的元素的位置。
+ /// 删除元素后的数组。
+ public static object SetArrayDeleteLocationElement(object Arr, int DeleteElementLocation)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ temp.RemoveAt(DeleteElementLocation);
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+ return temp;
+ }
+ ///
+ /// 在数组中寻找第一个匹配的元素,并返回其索引位置。
+ ///
+ /// 要搜索的数组。
+ /// 要寻找的元素。
+ /// 要寻找的元素在数组中的第一个匹配项的索引;如果未找到匹配项,则为 -1。
+ public static int ArrayQueryFirstMatchElement(object Arr, object QueryElement)
+ {
+ if (Arr is Array)
+ {
+ return Array.IndexOf((dynamic)Arr, (dynamic)QueryElement);
+ }
+ return ((dynamic)Arr).IndexOf((dynamic)QueryElement);
+ }
+ ///
+ /// 替换数组中第一个匹配的元素为指定的新元素。
+ ///
+ /// 要进行替换操作的数组。
+ /// 要替换的元素。
+ /// 要替换为的新元素。
+ /// 替换后的数组。
+ public static object ArrayReplaceFirstMatchElement(object Arr, object FirstMatchElement, object ReplaceElement)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ int Location = ArrayQueryFirstMatchElement(Arr, FirstMatchElement);
+ temp[Location] = (dynamic)ReplaceElement;
+ return temp;
+ }
+ ///
+ /// 将数组中所有匹配的元素替换为指定的新元素。
+ ///
+ /// 要进行替换操作的数组。
+ /// 要替换的元素。
+ /// 要替换为的新元素。
+ /// 替换后的数组。
+ public static object ArrayReplaceAllMatchElement(object Arr, object MatchElement, object ReplaceElement)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ int 长度 = temp.Count;
+
+ for (int i = 0; i < 长度; i++)
+ {
+ if (((dynamic)Arr)[i] == (dynamic)MatchElement)
+ {
+ ((dynamic)Arr)[i] = (dynamic)ReplaceElement;
+ }
+ }
+ return Arr;
+ }
+ ///
+ /// 反转数组中元素的顺序。
+ ///
+ /// 要反转的数组。
+ /// 反转后的数组。
+ public static object ArrayReverse(object Arr)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ temp.Reverse();
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+ return temp;
+ }
+ ///
+ /// 对数组进行排序。
+ ///
+ /// 要排序的数组。
+ /// 排序后的数组。
+ public static object ArraySorting(object Arr)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ temp = Enumerable.ToList(Enumerable.Order(temp));
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+ return temp;
+ }
+ ///
+ /// 获取数组的长度。
+ ///
+ /// 要获取长度的数组。
+ /// 数组的长度。
+ public static int GetArrayLength(object Arr)
+ {
+ if (Arr is Array)
+ {
+ return ((Array)Arr).Length;
+ }
+ return ((ICollection)Arr).Count;
+
+ }
+ ///
+ /// 将对象转换为数组。
+ ///
+ /// 要转换的对象。
+ /// 转换后的数组。
+ public static object objectConvertToArray(object Arr)
+ {
+ return Enumerable.ToArray((dynamic)Arr);
+ }
+ ///
+ /// 将对象转换为列表。
+ ///
+ /// 要转换的对象。
+ /// 转换后的列表。
+ public static object objectConvertToList(object Arr)
+ {
+ return Enumerable.ToList((dynamic)Arr);
+ }
+ ///
+ /// 从数组中截取指定长度的子数组。
+ ///
+ /// 要截取的数组。
+ /// 截取开始的位置。
+ /// 要截取的长度。
+ /// 截取得到的子数组。
+ public static object ArrayCapture(object Arr, int StartLocation, int Length)
+ {
+ var temp = Enumerable.ToList((dynamic)Arr);
+ temp = Enumerable.ToList(Enumerable.Take(Enumerable.Skip(temp, StartLocation), Length));
+ if (Arr is Array)
+ {
+ return Enumerable.ToArray(temp);
+ }
+ return temp;
+ }
+
+ #endregion
+
+ }
+}
diff --git a/Command/CommandMath.cs b/Command/CommandMath.cs
new file mode 100644
index 0000000..7fdf302
--- /dev/null
+++ b/Command/CommandMath.cs
@@ -0,0 +1,473 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+ public enum ComparisonFuncEnum
+ {
+ Big,
+ BigOrEqual,
+ Small,
+ SmallOrEqual,
+ Equal,
+ NotEqual
+ }
+
+ [ACPCommand]
+ public static class CommandMath
+ {
+ #region 返回值是Double类型数据
+ ///
+ /// 两数相加(Param1 + Param2)
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数2
+ /// Param1:9 Param2:2 返回值:11
+ ///
+ public static double ParamAdd(double Param1, double Param2)
+ {
+ return Param1 + Param2;
+ }
+
+ ///
+ /// 两数相减(Param1 - Param2)
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数2
+ /// Param1:9 Param2:2 返回值:7
+ ///
+ public static double ParamReduce(double Param1, double Param2)
+ {
+ return Param1 - Param2;
+ }
+
+ ///
+ /// 两数相乘(Param1 * Param2)
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数2
+ /// Param1:9 Param2:2 返回值:18
+ ///
+ public static double ParamMult(double Param1, double Param2)
+ {
+ return Param1 * Param2;
+ }
+
+ ///
+ /// 两数相除(Param1 / Param2)
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数2
+ /// 传入值:保留几位小数
+ /// Param1:10 Param2:3 Param3:3 返回值:3.333
+ /// Param1:11 Param2:3 Param3:2 返回值:3.67
+ ///
+ public static double ParamDivide(double Param1, double Param2, int Param3)
+ {
+ return Math.Round((Param1 / Param2), Param3);
+ }
+
+ ///
+ /// 两数相除求余(Param1 % Param2)
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数2
+ /// Param1:9 Param2:2 返回值:1
+ ///
+ public static double ParamRemainder(double Param1, double Param2)
+ {
+ return Param1 % Param2;
+ }
+
+ ///
+ /// 实数开根号
+ ///
+ /// 传入值:实数1
+ /// Param1:16 返回值:4
+ ///
+ public static double ParamSquareRoot(double Param1)
+ {
+ return Math.Sqrt(Param1);
+ }
+
+ ///
+ /// 实数的幂运算
+ ///
+ /// 传入值:实数1
+ /// 传入值:实数1需要做几次方
+ /// Param1:3 Param2:2 返回值:9
+ /// Param1:2 Param2:3 返回值:8
+ /// Param1:5 Param2:4 返回值:625
+ ///
+ public static double ParamPow(double Param1, double Param2)
+ {
+ return Math.Pow(Param1, Param2);
+ }
+
+ ///
+ /// 实数的绝对值
+ ///
+ /// 传入值:实数1
+ /// Param1:-3.7 返回值:3.7
+ ///
+ public static double ParamAbs(double Param1)
+ {
+ return Math.Abs(Param1);
+ }
+
+ ///
+ /// 实数的向下取整
+ ///
+ /// 传入值:实数1
+ /// Param1:3.7 返回值:3
+ ///
+ public static double ParamFloor(double Param1)
+ {
+ return Math.Floor(Param1);
+ }
+
+ ///
+ /// 实数的向上取整
+ ///
+ /// 传入值:实数1
+ /// Param1:3.7 返回值:4
+ ///
+ public static double ParamCeiling(double Param1)
+ {
+ return Math.Ceiling(Param1);
+ }
+ #endregion
+
+
+ #region 返回值是String类型的数据
+ ///
+ /// A与B转换为double后比较,返回A比较B的结果
+ ///
+ /// 比较数1
+ /// 比较数2
+ /// 比较方法
+ /// 返回A比较B
+ public static bool Comparison(string a, string b, ComparisonFuncEnum 比较方法)
+ {
+ var aa = Convert.ToDouble(a);
+ var bb = Convert.ToDouble(b);
+
+ switch (比较方法)
+ {
+ case ComparisonFuncEnum.Big:
+ return aa > bb;
+ case ComparisonFuncEnum.BigOrEqual:
+ return aa >= bb;
+ case ComparisonFuncEnum.Small:
+ return aa < bb;
+ case ComparisonFuncEnum.SmallOrEqual:
+ return aa <= bb;
+ case ComparisonFuncEnum.Equal:
+ return aa == bb;
+ case ComparisonFuncEnum.NotEqual:
+ return aa != bb;
+ default:
+ return false;
+ }
+ }
+
+ ///
+ /// 加法指令,转换为double后返回o1+o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 返回o1+o2
+ public static string TwoNumberAdd(string o1, string o2)
+ {
+ return (Convert.ToDecimal(o1) + Convert.ToDecimal(o2)).ToString();
+ }
+ ///
+ /// 减法指令,转换为double后返回o1-o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 返回o1-o2
+ public static string TwoNumberReduce(string o1, string o2)
+ {
+ return (Convert.ToDecimal(o1) - Convert.ToDecimal(o2)).ToString();
+ }
+ ///
+ /// 乘法指令,转换为double后返回o1*o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 返回o1*o2
+ public static string TwoNumberMult(string o1, string o2)
+ {
+ return (Convert.ToDecimal(o1) * Convert.ToDecimal(o2)).ToString();
+ }
+ ///
+ /// 除法指令,转换为double后返回o1/o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 返回o1/o2
+ public static string TwoNumberDivide1(string o1, string o2)
+ {
+ return (Convert.ToDecimal(o1) / Convert.ToDecimal(o2)).ToString();
+ }
+ ///
+ /// 转换为double后返回o1/o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 保留几位小数
+ /// 返回o1/o2
+ public static string TwoNumberDivide2(string o1, string o2, int o3)
+ {
+ return (Math.Round((Convert.ToDecimal(o1) / Convert.ToDecimal(o2)), o3)).ToString();
+ }
+ ///
+ /// 返回o1%o2
+ ///
+ /// 运算数1
+ /// 运算数2
+ /// 返回o1%o2
+ public static string TwoNumberRemainder(string o1, string o2)
+ {
+ return (Convert.ToDecimal(o1) % (Convert.ToDecimal(o2))).ToString();
+ }
+ ///
+ /// 计算两个数字的平方。
+ ///
+ /// 第一个数字的字符串表示。
+ /// 第二个数字的字符串表示。
+ /// Param1:3 Param2:2 返回值:9
+ /// Param1:2 Param2:3 返回值:8
+ /// Param1:5 Param2:4 返回值:625
+ /// 返回两个数字的平方的字符串表示。
+ public static string NumberPow(string o1, string o2)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算它们的平方
+ // 最后将结果转换为字符串并返回
+ return Math.Pow(Convert.ToDouble(o1), Convert.ToDouble(o2)).ToString();
+ }
+ ///
+ /// 计算一个数的平方根。
+ ///
+ /// 要计算平方根的数字的字符串表示。
+ /// 返回输入数字的平方根的字符串表示。
+ public static string NumberSqrt(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其平方根。
+ // 最后将结果转换为字符串并返回。
+ return Math.Sqrt(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数的绝对值。
+ ///
+ /// 要计算绝对值的数字的字符串表示。
+ /// 返回输入数字的绝对值的字符串表示。
+ public static string NumberAbs(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其绝对值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Abs(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个角度的正弦值。
+ ///
+ /// 要计算正弦值的角度的字符串表示(单位为弧度)。
+ /// 返回输入角度的正弦值的字符串表示。
+ public static string NumberSin(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其正弦值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Sin(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个角度的余弦值。
+ ///
+ /// 要计算余弦值的角度的字符串表示(单位为弧度)。
+ /// 返回输入角度的余弦值的字符串表示。
+ public static string NumberCos(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其余弦值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Cos(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个角度的正切值。
+ ///
+ /// 要计算正切值的角度的字符串表示(单位为弧度)。
+ /// 返回输入角度的正切值的字符串表示。
+ public static string NumberTan(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其正切值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Tan(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数的反正弦值。
+ ///
+ /// 要计算反正弦值的数的字符串表示。
+ /// 返回输入数的反正弦值的字符串表示(单位为弧度)。
+ public static string NumberASin(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其反正弦值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Asin(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数的反余弦值。
+ ///
+ /// 要计算反余弦值的数的字符串表示。
+ /// 返回输入数的反余弦值的字符串表示(单位为弧度)。
+ public static string NumberACos(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其反余弦值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Acos(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数的反正切值。
+ ///
+ /// 要计算反正切值的数的字符串表示。
+ /// 返回输入数的反正切值的字符串表示(单位为弧度)。
+ public static string NumberATan(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其反正切值。
+ // 最后将结果转换为字符串并返回。
+ return Math.Atan(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算 e 的给定次幂。
+ ///
+ /// e 的指数的字符串表示。
+ /// 返回 e 的给定次幂的字符串表示。
+ public static string eIndex(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算 e 的给定次幂。
+ // 最后将结果转换为字符串并返回。
+ return Math.Exp(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数的自然对数。
+ ///
+ /// 要计算自然对数的数的字符串表示。
+ /// 返回输入数的自然对数的字符串表示。
+ public static string NumberNaturalLogarithm(string o1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其自然对数。
+ // 最后将结果转换为字符串并返回。
+ return Math.Log(Convert.ToDouble(o1)).ToString();
+ }
+ ///
+ /// 计算一个数在指定基数下的对数。
+ ///
+ /// 要计算对数的数的字符串表示。
+ /// 对数的基数的字符串表示。
+ /// 返回输入数在指定基数下的对数的字符串表示。
+ public static string NumberLogarithm(string o1, string Base)
+ {
+ // 将输入的字符串转换为双精度浮点数,并计算其在指定基数下的对数。
+ // 最后将结果转换为字符串并返回。
+ return Math.Log(Convert.ToDouble(o1), Convert.ToDouble(Base)).ToString();
+ }
+ ///
+ /// 对输入数字进行四舍五入取整。
+ ///
+ /// 要进行取整操作的数字的字符串表示。
+ /// 返回经过四舍五入取整后的结果的字符串表示。
+ public static string NumberRoundToNearest(string n1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并进行四舍五入取整。
+ // 最后将结果转换为字符串并返回。
+ return Math.Round(Convert.ToDouble(n1)).ToString();
+ }
+
+ ///
+ /// 对输入数字进行向上舍入取整。
+ ///
+ /// 要进行取整操作的数字的字符串表示。
+ /// 返回经过向上舍入取整后的结果的字符串表示。
+ public static string NumberRoundUp(string n1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并进行向上舍入取整。
+ // 最后将结果转换为字符串并返回。
+ return Math.Ceiling(Convert.ToDouble(n1)).ToString();
+ }
+
+ ///
+ /// 对输入数字进行向下舍入取整。
+ ///
+ /// 要进行取整操作的数字的字符串表示。
+ /// 返回经过向下舍入取整后的结果的字符串表示。
+ public static string NumberRoundDown(string n1)
+ {
+ // 将输入的字符串转换为双精度浮点数,并进行向下舍入取整。
+ // 最后将结果转换为字符串并返回。
+ return Math.Floor(Convert.ToDouble(n1)).ToString();
+ }
+
+ ///
+ /// 获取圆周率π的字符串表示。
+ ///
+ /// 返回圆周率π的字符串表示。
+ public static string PI()
+ {
+ // 返回圆周率π的字符串表示。
+ return Math.PI.ToString();
+ }
+
+ //public static Random random1 = new Random();
+ ///
+ /// 生成一个随机整数。
+ ///
+ /// 返回生成的随机整数的字符串表示。
+ public static string RandomNumber_Int()
+ {
+ Random random = new Random();
+ // 使用随机数生成器生成一个随机整数,并将其转换为字符串表示。
+ return random.Next().ToString();
+ }
+
+ ///
+ /// 生成一个指定范围内的随机整数。
+ ///
+ /// 随机数生成范围的最小值。
+ /// 随机数生成范围的最大值(不包含)。
+ /// 返回生成的指定范围内的随机整数的字符串表示。
+ public static string RandomNumber_RangeInt(int 最小, int 最大)
+ {
+ Random random = new Random();
+ // 使用随机数生成器生成一个指定范围内的随机整数,并将其转换为字符串表示。
+ return random.Next(最小, 最大).ToString();
+ }
+
+ ///
+ /// 生成一个随机小数。
+ ///
+ /// 返回生成的随机小数的字符串表示。
+ public static string RandomNumber_Decimal()
+ {
+ Random random = new Random();
+ // 使用随机数生成器生成一个随机小数,并将其转换为字符串表示。
+ return random.NextDouble().ToString();
+ }
+
+ ///
+ /// 计算给定算式的结果。
+ ///
+ /// 要计算的算式的字符串表示。
+ /// 返回计算结果的字符串表示。
+ public static string FormulaCalculation(string s)
+ {
+ // 创建一个 DataTable 实例来进行算式的计算。
+ DataTable dataTable = new DataTable();
+ // 使用 DataTable 的 Compute 方法计算给定的算式,并将结果转换为字符串表示。
+ return dataTable.Compute(s, "").ToString();
+ }
+ #endregion
+ }
+}
diff --git a/Command/CommandRadixChange.cs b/Command/CommandRadixChange.cs
new file mode 100644
index 0000000..d3402e3
--- /dev/null
+++ b/Command/CommandRadixChange.cs
@@ -0,0 +1,153 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+
+ [ACPCommand]
+ public static class CommandRadixChange
+ {
+ #region 进制转换
+ ///
+ /// 二进制 转换成 八进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"101010" 返回值:"52"
+ ///
+ public static string TwoRadix_ConvertTo_EightRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 2),8);
+ }
+
+ ///
+ /// 二进制 转换成 十进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"101010" 返回值:"42"
+ ///
+ public static string TwoRadix_ConvertTo_TenRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 2));
+ }
+
+ ///
+ /// 二进制 转换成 十六进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"101010" 返回值:"2A"
+ ///
+ public static string TwoRadix_ConvertTo_SixteenRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 2), 16);
+ }
+
+ ///
+ /// 八进制 转换成 二进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"88" 返回值:"1001000"
+ ///
+ public static string EightRadix_ConvertTo_TwoRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 8), 2);
+ }
+
+ ///
+ /// 八进制 转换成 十进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"88" 返回值:"72"
+ ///
+ public static double EightRadix_ConvertTo_TenRadix(string Param1)
+ {
+ double Value = Convert.ToInt32(Param1, 8);
+ return Value;
+ }
+
+ ///
+ /// 八进制 转换成 十六进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"88" 返回值:"48"
+ ///
+ public static string EightRadix_ConvertTo_SixteenRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 8), 16);
+ }
+
+ ///
+ /// 十进制 转换成 二进制
+ ///
+ /// 传入值:整型数值
+ /// Param1:60 返回值:"111100"
+ ///
+ public static string TenRadix_ConvertTo_TwoRadix(int Param1)
+ {
+ return Convert.ToString(Param1, 2);
+ }
+
+ ///
+ /// 十进制 转换成 八进制
+ ///
+ /// 传入值:整型数值
+ /// Param1:60 返回值:"74"
+ ///
+ public static string TenRadix_ConvertTo_EightRadix(int Param1)
+ {
+ return Convert.ToString(Param1, 8);
+ }
+
+ ///
+ /// 十进制 转换成 十六进制
+ ///
+ /// 传入值:整型数值
+ /// Param1:60 返回值:"3C"
+ ///
+ public static string TenRadix_ConvertTo_SixteenRadix(int Param1)
+ {
+ return Convert.ToString(Param1, 16);
+ }
+
+ ///
+ /// 十六进制 转换成 二进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"6F" 返回值:"1101111"
+ ///
+ public static string SixteenRadix_ConvertTo_Radix2(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 16), 2);
+ }
+
+ ///
+ /// 十六进制 转换成 八进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"6F" 返回值:"157"
+ ///
+ public static string SixteenRadix_ConvertTo_EightRadix8(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 16), 8);
+ }
+
+ ///
+ /// 十六进制 转换成 十进制
+ ///
+ /// 传入值:字符类型
+ /// Param1:"6F" 返回值:"111"
+ ///
+ public static string SixteenRadix_ConvertTo_TenRadix(string Param1)
+ {
+ return Convert.ToString(Convert.ToInt32(Param1, 16));
+ }
+
+
+ #endregion
+
+ }
+}
diff --git a/Command/CommandStringProcessing.cs b/Command/CommandStringProcessing.cs
new file mode 100644
index 0000000..bf88054
--- /dev/null
+++ b/Command/CommandStringProcessing.cs
@@ -0,0 +1,196 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+
+ [ACPCommand]
+ public static class CommandStringProcessing
+ {
+ ///
+ ///获取值
+ ///
+ ///要获取的值。
+ ///returns>获取值(字符串)。
+ public static string GetValue(object obj)
+ {
+ return Convert.ToString(obj) ?? string.Empty;
+ }
+
+ ///
+ /// 在原字符串中查找指定字符的位置。
+ ///
+ /// 要查找的字符串。
+ /// 要查找的字符。
+ /// 字符在字符串中的位置,如果未找到则返回 -1。
+ public static int FindStr(string str, string findStr)
+ {
+ return str.IndexOf(findStr);
+ }
+
+ ///
+ /// 将原字符串中的指定字符替换为新字符。
+ ///
+ /// 要替换的字符串。
+ /// 要被替换的字符。
+ /// 替换后的新字符。
+ /// 替换后的字符串。
+ public static string ReplaceStr(string str, string ReplaceStr, string ReplacedStr)
+ {
+ return str.Replace(ReplaceStr, ReplacedStr);
+ }
+
+ ///
+ /// 从原字符串中移除指定位置开始的指定个数的字符。
+ ///
+ /// 要操作的字符串。
+ /// 开始移除字符的位置。
+ /// 要移除的字符个数。
+ /// 移除后的字符串。
+ public static string RemoveStr(string str, int startLocation, int removeQty)
+ {
+ return str.Remove(startLocation, removeQty);
+ }
+
+ ///
+ /// 在原字符串的指定位置插入新字符串。
+ ///
+ /// 要操作的字符串。
+ /// 要插入字符串的位置。
+ /// 要插入的字符串。
+ /// 插入后的字符串。
+ public static string InsertStr(string str, int startLocation, string newStr)
+ {
+ return str.Insert(startLocation, newStr);
+ }
+
+ ///
+ /// 从原字符串中截取指定位置开始的指定个数的字符。
+ ///
+ /// 要操作的字符串。
+ /// 要截取的起始位置。
+ /// 要截取的字符个数。
+ /// 截取后的字符串。
+ public static string CaptureStr(string str, int startLocation, int qty)
+ {
+ return str.Substring(startLocation, qty);
+ }
+
+ ///
+ /// 将字符串转换为小写形式。
+ ///
+ /// 要转换的字符串。
+ /// 转换为小写后的字符串。
+ public static string StrConvertToLower(string str)
+ {
+ return str.ToLower();
+ }
+
+ ///
+ /// 将字符串转换为大写形式。
+ ///
+ /// 要转换的字符串。
+ /// 转换为大写后的字符串。
+ public static string StrConvertToUpper(string str)
+ {
+ return str.ToUpper();
+ }
+
+ ///
+ /// 获取字符串的长度。
+ ///
+ /// 要获取长度的字符串。
+ /// 字符串的长度。
+ public static int GetStrLength(string str)
+ {
+ return str.Length;
+ }
+
+ ///
+ /// 去除字符串两端的空白字符。
+ ///
+ /// 要去除空白字符的字符串。
+ /// 去除空白字符后的字符串。
+ public static string StrRemoveLeadingAndTrailingWhitespaces(string str)
+ {
+ return str.Trim();
+ }
+
+ ///
+ /// 将字节数组转换为字符串,使用 UTF-8 编码。
+ ///
+ /// 要转换的字节数组。
+ /// 转换后的字符串。
+ public static string ByteArrayToStr_UTF8(byte[] arr)
+ {
+ return Encoding.UTF8.GetString(arr);
+ }
+
+ ///
+ /// 将字节数组转换为字符串,使用 GB18030 编码。
+ ///
+ /// 要转换的字节数组。
+ /// 转换后的字符串。
+ public static string ByteArrayToStr_GB18030(byte[] arr)
+ {
+ return Encoding.GetEncoding("gb18030").GetString(arr);
+ }
+
+ ///
+ /// 将字节数组转换为字符串,使用自选的编码类型。
+ ///
+ /// 要转换的字节数组。
+ /// 要使用的编码类型。
+ /// 转换后的字符串。
+ public static string ByteArrayToStr_SelfSelectedEncoding(byte[] arr, string encodingType)
+ {
+ return Encoding.GetEncoding(encodingType).GetString(arr);
+ }
+
+ ///
+ /// 将字节数组转换为16进制字符串
+ ///
+ /// 要转换的字节数组。
+ /// 转换后的字符串。
+ public static string ByteArrayTo_HexStr(byte[] str)
+ {
+ return Convert.ToInt32(str.ToString(),2).ToString("X2");
+ }
+
+ ///
+ /// 将字符串转换为字节数组,使用 UTF-8 编码。
+ ///
+ /// 要转换的字符串。
+ /// 转换后的字节数组。
+ public static byte[] StrToByteArray_UTF8(string str)
+ {
+ return Encoding.UTF8.GetBytes(str);
+ }
+ ///
+ /// 将字符串转换为字节数组,使用 GB18030 编码。
+ ///
+ /// 要转换的字符串。
+ /// 转换后的字节数组。
+ public static byte[] StrToByteArray_GB18030(string str)
+ {
+ return Encoding.GetEncoding("gb18030").GetBytes(str);
+ }
+
+ ///
+ /// 将字符串转换为字节数组,使用自选的编码类型。
+ ///
+ /// 要转换的字符串。
+ /// 要使用的编码类型。
+ /// 转换后的字节数组。
+ public static byte[] StrToByteArray_SelfSelectedEncoding(string str, string encodingType)
+ {
+ return Encoding.GetEncoding(encodingType).GetBytes(str);
+ }
+
+ }
+}
diff --git a/Command/CommandSystem.cs b/Command/CommandSystem.cs
new file mode 100644
index 0000000..8086dac
--- /dev/null
+++ b/Command/CommandSystem.cs
@@ -0,0 +1,463 @@
+using Common.Attributes;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
+namespace Command
+{
+ [ACPCommand]
+ public static class CommandSystem
+ {
+ #region 类型转换
+ ///
+ /// int转化为double
+ ///
+ /// 传入值:整型数值
+ /// Param1:40 返回值:40
+ ///
+ public static double Int_ConvertTo_Double(int Param1)
+ {
+ return Convert.ToDouble(Param1);
+ }
+
+ ///
+ /// double转化为Int16
+ ///
+ /// 传入值:实数数值
+ /// Param1:4 返回值:4
+ /// Param1:4.67 返回值:4
+ ///
+ public static Int16 Double_ConvertTo_Int16(double Param1)
+ {
+ Int16 Value = Convert.ToInt16(Param1);
+ return Value;
+ }
+
+ ///
+ /// double转化为int,即Int32
+ ///
+ /// 传入值:实数数值
+ /// Param1:4 返回值:4
+ /// Param1:4.67 返回值:4
+ ///
+ public static int Double_ConvertTo_Int(double Param1)
+ {
+ int Value = Convert.ToInt32(Param1);
+ return Value;
+ }
+
+ ///
+ /// double转化为Int64
+ ///
+ /// 传入值:实数数值
+ /// Param1:4 返回值:4
+ /// Param1:4.67 返回值:4
+ ///
+ public static Int64 Double_ConvertTo_Int64(double Param1)
+ {
+ Int64 Value = Convert.ToInt64(Param1);
+ return Value;
+ }
+
+ ///
+ /// float转化为double
+ ///
+ /// 传入值:浮点数值
+ /// Param1:4.0 返回值:4
+ /// Param1:4.2 返回值:4.2
+ ///
+ public static double Float_ConvertTo_Double(float Param1)
+ {
+ double Value = Convert.ToDouble(Param1);
+ return Value;
+ }
+
+ ///
+ /// 判断string转化为int,转换成功返回 true,失败返回 false
+ ///
+ /// 传入值:字符串
+ /// Param1:"2" 返回值:true
+ /// Param1:"abc" 返回值:false
+ ///
+ public static bool BoolString_ConvertTo_Int(string Param1)
+ {
+ bool Value = int.TryParse(Param1, out int intValue);
+ return Value;
+ }
+
+ ///
+ /// string转化为int
+ ///
+ /// 传入值:字符串
+ /// Param1:"2" 返回值:2
+ ///
+ public static int String_ConvertTo_Int(string Param1)
+ {
+ int Value = Convert.ToInt32(Param1);
+ return Value;
+ }
+
+ ///
+ /// int转化为string
+ ///
+ /// 传入值:整型数值
+ /// Param1:2 返回值:"2"
+ ///
+ public static string Int_ConvertTo_String(int Param1)
+ {
+ string Value = Convert.ToString(Param1);
+ return Value;
+ }
+
+ ///
+ /// 判断string转化为double,转换成功返回 true,失败返回 false
+ ///
+ /// 传入值:字符串
+ /// Param1:"2" 返回值:true
+ /// Param1:"2.1" 返回值:true
+ /// Param1:"abc" 返回值:false
+ ///
+ public static bool BoolString_ConvertTo_Double(string Param1)
+ {
+ bool Value = double.TryParse(Param1, out double intValue);
+ return Value;
+ }
+
+ ///
+ /// string转化为double
+ ///
+ /// 传入值:字符串
+ /// Param1:"2" 返回值:2
+ /// Param1:"2.1" 返回值:2.1
+ ///
+ public static double String_ConvertTo_Double(string Param1)
+ {
+ double Value = Convert.ToDouble(Param1);
+ return Value;
+ }
+
+ ///
+ /// double转化为string
+ ///
+ /// 传入值:实数数值
+ /// Param1:2 返回值:"2"
+ /// Param1:2.1 返回值:"2.1"
+ ///
+ public static string Double_ConvertTo_String(double Param1)
+ {
+ string Value = Convert.ToString(Param1);
+ return Value;
+ }
+
+ ///
+ /// float转化为string
+ ///
+ /// 传入值:浮点数值
+ /// Param1:2.0 返回值:"2.0"
+ /// Param1:2.1 返回值:"2.1"
+ ///
+ public static string Float_ConvertTo_String(float Param1)
+ {
+ string Value = Convert.ToString(Param1);
+ return Value;
+ }
+
+ ///
+ /// 判断string转化为datetime,转换成功返回 true,失败返回 false
+ ///
+ /// 传入值:时间字符串
+ /// Param1:"2025-08-14" 返回值:true
+ /// Param1:"2025-08-14 13:14:15" 返回值:true
+ /// Param1:"abc" 返回值:false
+ ///
+ public static bool BoolString_ConvertTo_Datetime(string Param1)
+ {
+ bool Value = DateTime.TryParse(Param1, out DateTime DateTimeValue);
+ return Value;
+ }
+
+ ///
+ /// string转化为datetime
+ ///
+ /// 传入值:时间字符串
+ /// Param1:"2025-08-14" 返回值:2025/8/14 0:00:00
+ /// Param1:"2025-08-14 13:14:15" 返回值:2025/8/14 13:14:15
+ ///
+ public static DateTime String_ConvertTo_Datetime(string Param1)
+ {
+ DateTime Value = Convert.ToDateTime(Param1);
+ return Value;
+ }
+
+ ///
+ /// datetime转化为string
+ ///
+ /// 传入值:时间
+ /// Param1:2025/8/14 13:14:15 返回值:"2025/08/14 13:14:15"
+ ///
+ public static string Datetime_ConvertTo_String1(DateTime Param1)
+ {
+ string Value = Convert.ToString(Param1);
+ return Value;
+ }
+
+ ///
+ /// datetime转化为string
+ ///
+ /// 传入值:时间
+ /// 传入值:字符串时间格式
+ /// Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd" 返回值:"2025-08-14"
+ /// Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd hh:mm:ss" 返回值:"2025-08-14 01:14:15"
+ /// Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyy-MM-dd HH:mm:ss" 返回值:"2025-08-14 13:14:15"
+ /// Param1:2025/8/14 13:14:15 DateTimeFormat:"yyyyMMdd" 返回值:"20250814"
+ ///
+ public static string Datetime_ConvertTo_String2(DateTime Param1,string DateTimeFormat)
+ {
+ string Value = Param1.ToString(DateTimeFormat);
+ return Value;
+ }
+
+ ///
+ /// bool转化为string
+ ///
+ /// 传入值:布尔值
+ /// Param1:true 返回值:"true"
+ /// Param1:false 返回值:"false"
+ ///
+ public static string Bool_ConvertTo_String(bool Param1)
+ {
+ string Value = Convert.ToString(Param1);
+ return Value;
+ }
+
+ ///
+ /// string转化为bool
+ ///
+ /// 传入值:字符串
+ /// Param1:"true" 返回值:true
+ /// Param1:"false" 返回值:false
+ /// Param1:"abc" 返回值:false
+ ///
+ public static bool String_ConvertTo_Bool(string Param1)
+ {
+ bool Value = bool.TryParse(Param1, out bool boolValue);
+ return Value;
+ }
+
+ ///
+ /// 将无符号整型转换为整型
+ ///
+ ///
+ ///
+ public static int Uint_ConvertTo_Int(uint u)
+ {
+ return (int)u;
+ }
+
+ ///
+ /// 将对象转换为字符串
+ ///
+ ///
+ ///
+ public static string Object_ConvertTo_String(object obj)
+ {
+ return Convert.ToString(obj);
+ }
+
+ ///
+ /// 将字符串内容转换为byte
+ ///
+ /// 要转换的内容
+ ///
+ public static byte String_ConvertTo_Byte(string s)
+ {
+ return Convert.ToByte(s);
+ }
+
+ ///
+ /// 转换为short(int16)
+ ///
+ /// 要转换的内容
+ ///
+ public static short String_ConvertTo_Short(string s)
+ {
+ return Convert.ToInt16(s);
+ }
+
+ ///
+ /// 将字符串转换为无符号整数。
+ ///
+ /// 要转换的字符串。
+ /// 转换后的无符号整数。
+ public static uint String_ConvertTo_Uint(string s)
+ {
+ return Convert.ToUInt32(s);
+ }
+
+ ///
+ /// 将指定进制的字符串转换为整数。
+ ///
+ /// 要转换的字符串。
+ /// 源字符串的进制。只能为2,8,10,16
+ /// 转换后的整数。
+ public static int XRadixString_ConvertTo_Int(string s, int XRadix)
+ {
+ return Convert.ToInt32(s, XRadix);
+ }
+ #endregion
+
+
+ #region 逻辑操作
+ ///
+ /// 执行整数的异或操作。
+ ///
+ /// 第一个整数。
+ /// 第二个整数。
+ /// 异或操作结果。
+ public static int XOR(int a, int b)
+ {
+ return a ^ b;
+ }
+
+ ///
+ /// 执行整数的位或操作。
+ ///
+ /// 第一个整数。
+ /// 第二个整数。
+ /// 位或操作结果。
+ public static int BitwiseOR(int a, int b)
+ {
+ return a | b;
+ }
+
+ ///
+ /// 执行整数的位与操作。
+ ///
+ /// 第一个整数。
+ /// 第二个整数。
+ /// 位与操作结果。
+ public static int BitwiseAND(int a, int b)
+ {
+ return a & b;
+ }
+
+ ///
+ /// 将整数向左移动指定位数。
+ ///
+ /// 要移动的整数。
+ /// 左移的位数。
+ /// 移动后的结果。
+ public static int LeftShift(int a, int BitwiseLeftShift)
+ {
+ return a << BitwiseLeftShift;
+ }
+ ///
+ /// 将整数向右移动指定位数。
+ ///
+ /// 要移动的整数。
+ /// 右移的位数。
+ /// 移动后的结果。
+ public static int RightShift(int a, int BitwiseRightShift)
+ {
+ return a >> BitwiseRightShift;
+ }
+ ///
+ /// 执行逻辑或操作。
+ ///
+ /// 第一个逻辑值。
+ /// 第二个逻辑值。
+ /// 逻辑或操作结果。
+ public static bool LogicalOR(bool a, bool b)
+ {
+ return a || b;
+ }
+
+ ///
+ /// 执行逻辑与操作。
+ ///
+ /// 第一个逻辑值。
+ /// 第二个逻辑值。
+ /// 逻辑与操作结果。
+ public static bool LogicalAND(bool a, bool b)
+ {
+ return a && b;
+ }
+
+ ///
+ /// 将字节的高低位对调。
+ ///
+ /// 要对调的字节。
+ /// 对调后的字节。
+ public static byte ByteHighAndLowBitSwap(byte b)
+ {
+ return (byte)(((b & 0x0F) << 4) | ((b & 0xF0) >> 4));
+ }
+
+ ///
+ /// 翻转字节数组中的元素顺序。
+ ///
+ /// 要翻转的字节数组。
+ /// 翻转后的字节数组。
+ public static byte[] ByteArrayReverse(byte[] b)
+ {
+ return b.Reverse().ToArray();
+ }
+
+ ///
+ /// 将当前线程挂起指定的时间。
+ ///
+ /// 要延时的时间,以毫秒为单位。
+ public static void Delay_ms(int time)
+ {
+ Thread.Sleep(time);
+ }
+
+ ///
+ /// 将当前线程挂起指定的时间。
+ ///
+ /// 要延时的时间,以秒为单位。
+ public static void Delay_s(int time)
+ {
+ Thread.Sleep(time * 1000);
+ }
+
+ ///
+ /// 将当前线程挂起指定的时间。
+ ///
+ /// 要延时的时间,以分钟为单位。
+ public static void Delay_min(int time)
+ {
+ Thread.Sleep(time * 1000 * 60);
+ }
+
+ ///
+ /// 将当前线程挂起指定的时间。
+ ///
+ /// 要延时的时间,以小时为单位。
+ public static void Delay_hour(int time)
+ {
+ Thread.Sleep(time * 1000 * 60 * 60);
+ }
+
+ ///
+ /// 将当前线程挂起指定的时间。
+ ///
+ /// 要延时的时间,以天为单位。
+ public static void Delay_day(int time)
+ {
+ Thread.Sleep(time * 1000 * 60 * 60 * 24);
+ }
+
+ #endregion
+
+
+
+ #region
+
+ #endregion
+ }
+}
diff --git a/Command/CommandTime.cs b/Command/CommandTime.cs
new file mode 100644
index 0000000..bac6629
--- /dev/null
+++ b/Command/CommandTime.cs
@@ -0,0 +1,95 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+
+ [ACPCommand]
+ public static class CommandTime
+ {
+ #region 时间处理
+ ///
+ /// 获取给定时间段的总毫秒数。
+ ///
+ /// 要获取总毫秒数的时间段。
+ /// 返回时间段的总毫秒数。
+ public static double GetTimePeriodMilliseconds(TimeSpan TimePeriod)
+ {
+ // 返回时间段的总毫秒数。
+ return TimePeriod.TotalMilliseconds;
+ }
+
+ ///
+ /// 获取给定时间段的总秒数。
+ ///
+ /// 要获取总秒数的时间段。
+ /// 返回时间段的总秒数。
+ public static double GetTimePeriodSeconds(TimeSpan TimePeriod)
+ {
+ // 返回时间段的总秒数。
+ return TimePeriod.TotalSeconds;
+ }
+
+ ///
+ /// 获取给定时间段的总分钟数。
+ ///
+ /// 要获取总分钟数的时间段。
+ /// 返回时间段的总分钟数。
+ public static double GetTimePeriodMinutes(TimeSpan TimePeriod)
+ {
+ // 返回时间段的总分钟数。
+ return TimePeriod.TotalMinutes;
+ }
+
+ ///
+ /// 获取给定时间段的总小时数。
+ ///
+ /// 要获取总小时数的时间段。
+ /// 返回时间段的总小时数。
+ public static double GetTimePeriodHours(TimeSpan TimePeriod)
+ {
+ // 返回时间段的总小时数。
+ return TimePeriod.TotalHours;
+ }
+
+ ///
+ /// 获取给定时间段的总天数。
+ ///
+ /// 要获取总天数的时间段。
+ /// 返回时间段的总天数。
+ public static double GetTimePeriodDays(TimeSpan TimePeriod)
+ {
+ // 返回时间段的总天数。
+ return TimePeriod.TotalDays;
+ }
+
+ ///
+ /// 计算两个日期时间之间的时间差。
+ ///
+ /// 第一个日期时间。
+ /// 第二个日期时间。
+ /// 返回两个日期时间之间的时间差。
+ public static TimeSpan GetDateReduce(DateTime date1, DateTime date2)
+ {
+ // 返回第一个日期时间减去第二个日期时间的时间差。
+ return date1 - date2;
+ }
+
+ ///
+ /// 获取当前系统时间。
+ ///
+ /// 返回当前系统时间。
+ public static DateTime GetNowTime()
+ {
+ // 返回当前系统时间。
+ return DateTime.Now;
+ }
+ #endregion
+
+ }
+}
diff --git a/Command/Delay.cs b/Command/Delay.cs
new file mode 100644
index 0000000..45215be
--- /dev/null
+++ b/Command/Delay.cs
@@ -0,0 +1,58 @@
+using Common.Attributes;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Command
+{
+ [ACPCommand]
+ public static class Delay
+ {
+ ///
+ /// 等待_毫秒
+ ///
+ ///
+ ///
+ ///
+ public static async Task Delay_ms(int millisecond, CancellationToken ct)
+ {
+ await Task.Delay(millisecond, ct);
+ }
+
+ ///
+ /// 等待_秒
+ ///
+ ///
+ ///
+ ///
+ public static async Task Delay_s(float second, CancellationToken ct)
+ {
+ await Task.Delay((int)second * 1000, ct);
+ }
+
+ ///
+ /// 等待_分钟
+ ///
+ ///
+ ///
+ ///
+ public static async Task Delay_m(float minnute, CancellationToken ct)
+ {
+ await Task.Delay((int)minnute * 60 * 1000, ct);
+ }
+
+ ///
+ /// 等待_小时
+ ///
+ ///
+ ///
+ ///
+ public static async Task Delay_h(float minnute, CancellationToken ct)
+ {
+ await Task.Delay((int)minnute * 60 * 1000, ct);
+ }
+ }
+}
diff --git a/Common/Attributes/ADPCommandAttribute.cs b/Common/Attributes/ADPCommandAttribute.cs
new file mode 100644
index 0000000..8425d61
--- /dev/null
+++ b/Common/Attributes/ADPCommandAttribute.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Common.Attributes
+{
+ ///
+ /// 标记可加载到指令集中的类或方法
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
+ public sealed class ACPCommandAttribute : Attribute
+ {
+ ///
+ /// 指令描述(用于工具提示)
+ ///
+ public string Description { get; }
+
+ ///
+ /// 指令分类(用于树形视图分组)
+ ///
+ public string Category { get; set; } = "默认分类";
+
+ public ACPCommandAttribute() { }
+
+ public ACPCommandAttribute(string description)
+ {
+ Description = description;
+ }
+ }
+}
diff --git a/Common/Attributes/MonitorableAttribute.cs b/Common/Attributes/MonitorableAttribute.cs
new file mode 100644
index 0000000..95f3970
--- /dev/null
+++ b/Common/Attributes/MonitorableAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Common.Attributes
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
+ public class MonitorableAttribute : Attribute
+ {
+ public string? Description { get; set; }
+
+ public MonitorableAttribute(string? description = null)
+ {
+ Description = description;
+ }
+ }
+}
diff --git a/Common/Common.csproj b/Common/Common.csproj
new file mode 100644
index 0000000..7b5d323
--- /dev/null
+++ b/Common/Common.csproj
@@ -0,0 +1,15 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
diff --git a/Common/MiniDump.cs b/Common/MiniDump.cs
new file mode 100644
index 0000000..9abaa41
--- /dev/null
+++ b/Common/MiniDump.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Common
+{
+ public static class MiniDump
+ {
+ [Flags]
+ public enum Option : uint
+ {
+ Normal = 0x00000000,
+ WithDataSegs = 0x00000001,
+ WithFullMemory = 0x00000002,
+ WithHandleData = 0x00000004,
+ FilterMemory = 0x00000008,
+ ScanMemory = 0x00000010,
+ WithUnloadedModules = 0x00000020,
+ WithIndirectlyReferencedMemory = 0x00000040,
+ FilterModulePaths = 0x00000080,
+ WithProcessThreadData = 0x00000100,
+ WithPrivateReadWriteMemory = 0x00000200,
+ WithoutOptionalData = 0x00000400,
+ WithFullMemoryInfo = 0x00000800,
+ WithThreadInfo = 0x00001000,
+ WithCodeSegs = 0x00002000,
+ WithoutAuxiliaryState = 0x00004000,
+ WithFullAuxiliaryState = 0x00008000,
+ WithPrivateWriteCopyMemory = 0x00010000,
+ IgnoreInaccessibleMemory = 0x00020000,
+ ValidTypeFlags = 0x0003ffff,
+ }
+
+ enum ExceptionInfo
+ {
+ None,
+ Present
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)] // Pack=4 is important! So it works also for x64!
+ struct MiniDumpExceptionInformation
+ {
+ public uint ThreadId;
+ public IntPtr ExceptionPointers;
+ [MarshalAs(UnmanagedType.Bool)]
+ public bool ClientPointers;
+ }
+
+ // MiniDumpWriteDump function declarations
+ [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
+ static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, ref MiniDumpExceptionInformation expParam, IntPtr userStreamParam, IntPtr callbackParam);
+
+ [DllImport("dbghelp.dll", EntryPoint = "MiniDumpWriteDump", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
+ static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, SafeHandle hFile, uint dumpType, IntPtr expParam, IntPtr userStreamParam, IntPtr callbackParam);
+
+ [DllImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", ExactSpelling = true)]
+ static extern uint GetCurrentThreadId();
+
+ static bool Write(SafeHandle fileHandle, Option options, ExceptionInfo exceptionInfo, Exception exception = null)
+ {
+ Process currentProcess = Process.GetCurrentProcess();
+ IntPtr currentProcessHandle = currentProcess.Handle;
+ uint currentProcessId = (uint)currentProcess.Id;
+
+ MiniDumpExceptionInformation exp;
+ exp.ThreadId = GetCurrentThreadId();
+ exp.ClientPointers = false;
+ exp.ExceptionPointers = IntPtr.Zero;
+
+ if (exceptionInfo == ExceptionInfo.Present && exception != null)
+ {
+ // Get exception pointers
+ exp.ExceptionPointers = Marshal.GetExceptionPointers();
+ }
+
+ // Write dump
+ bool result = exp.ExceptionPointers == IntPtr.Zero
+ ? MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)
+ : MiniDumpWriteDump(currentProcessHandle, currentProcessId, fileHandle, (uint)options, ref exp, IntPtr.Zero, IntPtr.Zero);
+
+ if (exception != null)
+ {
+
+ }
+
+ return result;
+ }
+
+
+
+ public static Boolean TryDump(String dmpPath, Option dmpType = Option.Normal, Exception exception = null)
+ {
+ var path = Path.Combine(Environment.CurrentDirectory, dmpPath);
+ var dir = Path.GetDirectoryName(path);
+ if (dir != null && !Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ using (var fs = new FileStream(path, FileMode.Create))
+ {
+ return Write(fs.SafeFileHandle, dmpType, exception == null ? ExceptionInfo.None : ExceptionInfo.Present, exception);
+ }
+ }
+ }
+
+}
diff --git a/Common/Tool/ExpressionEvaluator.cs b/Common/Tool/ExpressionEvaluator.cs
new file mode 100644
index 0000000..7b0bf17
--- /dev/null
+++ b/Common/Tool/ExpressionEvaluator.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using NCalc;
+
+namespace Common.Tools
+{
+ public class ExpressionEvaluator
+ {
+ public static bool EvaluateExpression(string expression, Dictionary? variables = null)
+ {
+ try
+ {
+ // 预处理:替换中文变量名为英文别名
+ var (processedExpression, processedVariables) = PreprocessExpression(expression, variables);
+
+ var expr = new Expression(processedExpression, EvaluateOptions.IgnoreCase);
+
+ if (processedVariables != null)
+ {
+ foreach (var kvp in processedVariables)
+ {
+ expr.Parameters[kvp.Key] = kvp.Value;
+ }
+ }
+
+ if (expr.HasErrors())
+ {
+ throw new ArgumentException($"条件表达式格式错误: {expr.Error}");
+ }
+
+ var result = expr.Evaluate();
+ return Convert.ToBoolean(result);
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException($"条件表达式异常: {ex.Message}");
+ }
+ }
+
+ private static (string, Dictionary) PreprocessExpression(
+ string expression,
+ Dictionary? variables)
+ {
+ if (variables == null || variables.Count == 0)
+ return (expression, variables ?? new Dictionary());
+
+ // 生成变量名映射 (中文 -> 英文别名)
+ var chineseToAlias = new Dictionary();
+ var aliasToValue = new Dictionary();
+ int counter = 1;
+
+ foreach (var key in variables.Keys)
+ {
+ if (ContainsChinese(key))
+ {
+ string alias = $"var_{counter}";
+ counter++;
+ chineseToAlias[key] = alias;
+ aliasToValue[alias] = variables[key];
+ }
+ }
+
+ // 如果没有中文变量名,直接返回原始数据
+ if (chineseToAlias.Count == 0)
+ return (expression, variables);
+
+ // 替换表达式中的中文变量名
+ string processedExpression = expression;
+ foreach (var pair in chineseToAlias)
+ {
+ // 使用正则确保完整匹配变量名
+ string pattern = $@"\b{Regex.Escape(pair.Key)}\b";
+ processedExpression = Regex.Replace(
+ processedExpression,
+ pattern,
+ pair.Value,
+ RegexOptions.Compiled
+ );
+ }
+
+ // 创建新变量字典(英文别名 + 原始英文变量)
+ var newVariables = new Dictionary(aliasToValue);
+ foreach (var key in variables.Keys)
+ {
+ if (!ContainsChinese(key))
+ {
+ newVariables[key] = variables[key];
+ }
+ }
+
+ return (processedExpression, newVariables);
+ }
+
+ // 检查字符串是否包含中文字符
+ private static bool ContainsChinese(string text)
+ {
+ return text.Any(c => c >= 0x4E00 && c <= 0x9FFF);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CurveModule/CurveModule.cs b/CurveModule/CurveModule.cs
new file mode 100644
index 0000000..13243e7
--- /dev/null
+++ b/CurveModule/CurveModule.cs
@@ -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();
+ }
+
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ containerRegistry.RegisterForNavigation("CurveRecallView");
+ }
+ }
+
+}
diff --git a/CurveModule/CurveModule.csproj b/CurveModule/CurveModule.csproj
new file mode 100644
index 0000000..be2fb1b
--- /dev/null
+++ b/CurveModule/CurveModule.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CurveModule/ViewModels/CurveRecallViewModel.cs b/CurveModule/ViewModels/CurveRecallViewModel.cs
new file mode 100644
index 0000000..888f4d6
--- /dev/null
+++ b/CurveModule/ViewModels/CurveRecallViewModel.cs
@@ -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
+{
+ ///
+ /// 曲线回读:加载 RecordView 导出的 CSV 文件,按 (MonitorName, Scope) 分组绘制曲线。
+ ///
+ public class CurveRecallViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
+ {
+ #region 属性
+ public bool KeepAlive => true;
+
+ public PlotModel Plot { get; }
+
+ /// 已加载的曲线通道列表
+ public ObservableCollection 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 命令处理
+ ///
+ /// 打开 CSV 文件对话框,解析数据并按 (MonitorName, Scope) 分组绘制曲线。
+ ///
+ 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 解析
+ ///
+ /// 解析 RecordView 导出的 CSV 文件,返回监测记录列表。
+ ///
+ private static List ParseCsv(string path)
+ {
+ var results = new List();
+
+ 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;
+ }
+
+ ///
+ /// 解析单行 CSV,正确处理引号转义。
+ ///
+ private static List ParseCsvLine(string line)
+ {
+ var fields = new List();
+ 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 构建
+ ///
+ /// 构建空的 OxyPlot 模型(与 MonitorView 完全一致的配置)。
+ ///
+ 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
+ }
+
+ ///
+ /// CSV 监测记录(单行数据)。
+ ///
+ 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; }
+ }
+
+ ///
+ /// 曲线回读通道:MonitorName + Scope 唯一标识一条曲线。
+ ///
+ 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; }
+ }
+}
diff --git a/CurveModule/Views/CurveRecallView.xaml b/CurveModule/Views/CurveRecallView.xaml
new file mode 100644
index 0000000..18ecbb9
--- /dev/null
+++ b/CurveModule/Views/CurveRecallView.xaml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CurveModule/Views/CurveRecallView.xaml.cs b/CurveModule/Views/CurveRecallView.xaml.cs
new file mode 100644
index 0000000..0b28627
--- /dev/null
+++ b/CurveModule/Views/CurveRecallView.xaml.cs
@@ -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
+{
+ ///
+ /// CurveRecallView.xaml 的交互逻辑
+ ///
+ public partial class CurveRecallView : UserControl
+ {
+ public CurveRecallView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/DeviceCommand/Base/IBaseInterface.cs b/DeviceCommand/Base/IBaseInterface.cs
new file mode 100644
index 0000000..ebffb50
--- /dev/null
+++ b/DeviceCommand/Base/IBaseInterface.cs
@@ -0,0 +1,16 @@
+using NModbus;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public interface IBaseInterface
+ {
+ public bool IsConnected { get; }
+ public Task ConnectAsync(CancellationToken ct = default);
+ public void Close();
+ }
+}
diff --git a/DeviceCommand/Base/IModbusDevice.cs b/DeviceCommand/Base/IModbusDevice.cs
new file mode 100644
index 0000000..360e843
--- /dev/null
+++ b/DeviceCommand/Base/IModbusDevice.cs
@@ -0,0 +1,19 @@
+using NModbus;
+
+
+namespace DeviceCommand.Base
+{
+ public interface IModbusDevice : IBaseInterface,IDisposable
+ {
+ IModbusMaster Modbus { get; }
+
+ Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default);
+ Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default);
+ Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
+
+ Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default);
+ Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
+
+ Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/ISerialPort.cs b/DeviceCommand/Base/ISerialPort.cs
new file mode 100644
index 0000000..e54a802
--- /dev/null
+++ b/DeviceCommand/Base/ISerialPort.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public interface ISerialPort : IBaseInterface,IDisposable
+ {
+ Task SendAsync(string data, CancellationToken ct = default);
+ Task ReadAsync(string delimiter = "\n", CancellationToken ct = default);
+ Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default);
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/ITcp.cs b/DeviceCommand/Base/ITcp.cs
new file mode 100644
index 0000000..e889668
--- /dev/null
+++ b/DeviceCommand/Base/ITcp.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public interface ITcp : IBaseInterface,IDisposable
+ {
+ Task SendAsync(byte[] buffer, CancellationToken ct = default);
+ Task SendAsync(string str, CancellationToken ct = default);
+ Task ReadAsync(int length, CancellationToken ct = default);
+ Task ReadAsync(string delimiter = "\n", CancellationToken ct = default);
+ Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default);
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/ModbusRtu.cs b/DeviceCommand/Base/ModbusRtu.cs
new file mode 100644
index 0000000..c9ceecb
--- /dev/null
+++ b/DeviceCommand/Base/ModbusRtu.cs
@@ -0,0 +1,164 @@
+using NModbus;
+using NModbus.Serial;
+using System;
+using System.IO.Ports;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public class ModbusRtu : IModbusDevice
+ {
+ public string PortName { get; private set; } = "COM1";
+ public int BaudRate { get; private set; } = 9600;
+ public int DataBits { get; private set; } = 8;
+ public StopBits StopBits { get; private set; } = StopBits.One;
+ public Parity Parity { get; private set; } = Parity.None;
+ public int ReadTimeout { get; private set; } = 3000;
+ public int WriteTimeout { get; private set; } = 3000;
+
+ private SerialPort _serialPort;
+ public IModbusMaster Modbus { get; private set; }
+ public bool IsConnected => _serialPort?.IsOpen ?? false;
+
+ protected readonly SemaphoreSlim _commLock = new(1, 1);
+
+ public ModbusRtu()
+ {
+ _serialPort = new SerialPort();
+ }
+
+ public void ConfigureDevice(string portName, int baudRate, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None, int readTimeout = 3000, int writeTimeout = 3000)
+ {
+ PortName = portName;
+ BaudRate = baudRate;
+ DataBits = dataBits;
+ StopBits = stopBits;
+ Parity = parity;
+ ReadTimeout = readTimeout;
+ WriteTimeout = writeTimeout;
+ }
+
+ public virtual async Task ConnectAsync(CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ if (_serialPort.IsOpen)
+ _serialPort.Close();
+
+ _serialPort.PortName = PortName;
+ _serialPort.BaudRate = BaudRate;
+ _serialPort.DataBits = DataBits;
+ _serialPort.StopBits = StopBits;
+ _serialPort.Parity = Parity;
+ _serialPort.ReadTimeout = ReadTimeout;
+ _serialPort.WriteTimeout = WriteTimeout;
+ _serialPort.Open();
+
+ Modbus = new ModbusFactory().CreateRtuMaster(_serialPort);
+ return true;
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public virtual void Close()
+ {
+ if (_serialPort.IsOpen)
+ _serialPort.Close();
+ }
+
+ public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
+ .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values)
+ .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await Modbus.WriteSingleCoilAsync(slaveAddress, coilAddress, value)
+ .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadInputRegistersAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public void Dispose()
+ {
+ _serialPort?.Dispose();
+ _commLock?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/ModbusTcp.cs b/DeviceCommand/Base/ModbusTcp.cs
new file mode 100644
index 0000000..1e370df
--- /dev/null
+++ b/DeviceCommand/Base/ModbusTcp.cs
@@ -0,0 +1,163 @@
+using Model.Models;
+using NModbus;
+using System;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public class ModbusTcp : IModbusDevice
+ {
+ public string IPAddress { get; private set; } = "127.0.0.1";
+ public int Port { get; private set; } = 502;
+ public int SendTimeout { get; private set; } = 3000;
+ public int ReceiveTimeout { get; private set; } = 3000;
+
+ private TcpClient _tcpClient;
+ public IModbusMaster Modbus { get; private set; }
+ public bool IsConnected => _tcpClient?.Connected ?? false;
+
+ 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()
+ {
+ _tcpClient = new TcpClient();
+ }
+
+ public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
+ {
+ IPAddress = ipAddress;
+ Port = port;
+ SendTimeout = sendTimeout;
+ ReceiveTimeout = receiveTimeout;
+ }
+
+ public virtual async Task ConnectAsync(CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ if (_tcpClient.Connected)
+ {
+ var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
+ if (remoteEndPoint.Address.MapToIPv4().ToString() == IPAddress && remoteEndPoint.Port == Port)
+ return true;
+ }
+
+ _tcpClient.Close();
+ _tcpClient.Dispose();
+ _tcpClient = new TcpClient();
+
+ await _tcpClient.ConnectAsync(IPAddress, Port, ct);
+ Modbus = new ModbusFactory().CreateMaster(_tcpClient);
+ return true;
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public virtual void Close()
+ {
+ if (_tcpClient.Connected) _tcpClient.Close();
+ }
+
+ public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ // 修复:FromMinutes 改为 FromMilliseconds
+ await Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
+ .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values)
+ .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await Modbus.WriteSingleCoilAsync(slaveAddress, coilAddress, value)
+ .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await Modbus.ReadInputRegistersAsync(slaveAddress, startAddress, numberOfPoints)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public void Dispose()
+ {
+ _tcpClient?.Dispose();
+ _commLock?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/Serial_Port.cs b/DeviceCommand/Base/Serial_Port.cs
new file mode 100644
index 0000000..1d73cb0
--- /dev/null
+++ b/DeviceCommand/Base/Serial_Port.cs
@@ -0,0 +1,227 @@
+using Model.Models;
+using System;
+using System.IO;
+using System.IO.Ports;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public class Serial_Port : ISerialPort, IDisposable
+ {
+ public string PortName { get; set; } = "COM1";
+ public int BaudRate { get; set; } = 9600;
+ public int DataBits { get; set; } = 8;
+ public StopBits StopBits { get; set; } = StopBits.One;
+ public Parity Parity { get; set; } = Parity.None;
+ public int ReadTimeout { get; set; } = 3000;
+ public int WriteTimeout { get; set; } = 3000;
+
+ private SerialPort _serialPort;
+ public bool IsConnected => _serialPort?.IsOpen ?? false;
+ protected readonly SemaphoreSlim commLock = new(1, 1);
+
+ public Serial_Port()
+ {
+ _serialPort = new SerialPort();
+ }
+
+ ///
+ /// 通过 一次性配置串口通信参数。
+ ///
+ public Serial_Port(SerialPortConfig config) : this()
+ {
+ if (config == null) return;
+ ConfigureDevice(config.PortName, config.BaudRate, config.DataBits, config.StopBits, config.Parity, config.ReadTimeout, config.WriteTimeout);
+ }
+
+ public void ConfigureDevice(string portName, int baudRate, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None, int readTimeout = 3000, int writeTimeout = 3000)
+ {
+ PortName = portName;
+ BaudRate = baudRate;
+ DataBits = dataBits;
+ StopBits = stopBits;
+ Parity = parity;
+ ReadTimeout = readTimeout;
+ WriteTimeout = writeTimeout;
+ }
+
+ public virtual async Task ConnectAsync(CancellationToken ct = default)
+ {
+ await commLock.WaitAsync(ct);
+ try
+ {
+ if (_serialPort.IsOpen) _serialPort.Close();
+
+ _serialPort.PortName = PortName;
+ _serialPort.BaudRate = BaudRate;
+ _serialPort.DataBits = DataBits;
+ _serialPort.StopBits = StopBits;
+ _serialPort.Parity = Parity;
+
+ // 允许底层保留默认设置,控制权交给外层 WaitAsync
+ _serialPort.ReadTimeout = SerialPort.InfiniteTimeout;
+ _serialPort.WriteTimeout = SerialPort.InfiniteTimeout;
+
+ _serialPort.Open();
+ return true;
+ }
+ finally
+ {
+ commLock.Release();
+ }
+ }
+
+ public virtual void Close()
+ {
+ if (_serialPort?.IsOpen == true) _serialPort.Close();
+ }
+
+ private async Task LoglessSendAsync(string data, CancellationToken ct)
+ {
+ if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。");
+
+ byte[] bytes = Encoding.UTF8.GetBytes(data);
+
+ try
+ {
+ if (WriteTimeout > 0)
+ {
+ // 核心修改:使用 WaitAsync 精准控制写入超时
+ await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, ct)
+ .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, ct).ConfigureAwait(false);
+ }
+ }
+ catch (TimeoutException ex)
+ {
+ throw new TimeoutException($"串口写入数据超时({WriteTimeout} ms)", ex);
+ }
+ }
+
+ public async Task SendAsync(string data, CancellationToken ct = default)
+ {
+ await commLock.WaitAsync(ct);
+ try
+ {
+ await LoglessSendAsync(data, ct);
+ }
+ finally
+ {
+ commLock.Release();
+ }
+ }
+
+ private async Task LoglessReadAsync(string delimiter, CancellationToken ct)
+ {
+ if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。");
+
+ delimiter ??= "\n";
+ var sb = new StringBuilder();
+ byte[] buffer = new byte[1024];
+
+ try
+ {
+ while (true)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ int bytesRead;
+ if (ReadTimeout > 0)
+ {
+ // 核心修改:使用 WaitAsync 精准控制单次异步读取超时
+ bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, ct)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
+ }
+
+ if (bytesRead == 0) continue;
+
+ sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
+
+ string currentText = sb.ToString();
+ int index = currentText.IndexOf(delimiter, StringComparison.Ordinal);
+ if (index >= 0)
+ {
+ return currentText.Substring(0, index).Trim();
+ }
+ }
+ }
+ catch (TimeoutException ex)
+ {
+ // 关键防污染策略:串口发生命令错、超时不响应时,WaitAsync 会立功捕获异常
+ // 我们直接清空串口驱动内部的输入缓冲区,把残余垃圾数据物理抹除,确保下一条指令安全、不掉线
+ ClearHardwareBuffer();
+ throw new TimeoutException($"串口读取超时(未收到结束符 '{delimiter}'),等待时间:{ReadTimeout} ms", ex);
+ }
+ }
+
+ public async Task ReadAsync(string delimiter = "\n", CancellationToken ct = default)
+ {
+ await commLock.WaitAsync(ct);
+ try
+ {
+ return await LoglessReadAsync(delimiter, ct);
+ }
+ finally
+ {
+ commLock.Release();
+ }
+ }
+
+ public async Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
+ {
+ await commLock.WaitAsync(ct);
+ try
+ {
+ // 发送新命令前先冲洗一下缓冲区,双重保险
+ ClearHardwareBuffer();
+
+ await LoglessSendAsync(command, ct);
+ return await LoglessReadAsync(delimiter, ct);
+ }
+ finally
+ {
+ commLock.Release();
+ }
+ }
+
+ ///
+ /// 物理强制清空串口驱动层及软件层的缓冲区
+ ///
+ private void ClearHardwareBuffer()
+ {
+ try
+ {
+ if (_serialPort?.IsOpen == true)
+ {
+ _serialPort.DiscardInBuffer(); // 清空硬件接收缓冲区
+ _serialPort.DiscardOutBuffer(); // 清空硬件发送缓冲区
+ }
+ }
+ catch
+ {
+ // 忽略清理异常
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_serialPort != null)
+ {
+ if (_serialPort.IsOpen) _serialPort.Close();
+ _serialPort.Dispose();
+ }
+ commLock?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Base/TCP.cs b/DeviceCommand/Base/TCP.cs
new file mode 100644
index 0000000..ab5b4fe
--- /dev/null
+++ b/DeviceCommand/Base/TCP.cs
@@ -0,0 +1,322 @@
+using Model.Models;
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Base
+{
+ public class Tcp : ITcp, IDisposable
+ {
+ public string IPAddress { get; set; } = "127.0.0.1";
+ public int Port { get; set; } = 502;
+ public int SendTimeout { get; set; } = 3000;
+ public int ReceiveTimeout { get; set; } = 3000;
+
+ private TcpClient _tcpClient;
+ public bool IsConnected => _tcpClient?.Connected ?? false;
+ protected readonly SemaphoreSlim _commLock = new(1, 1);
+
+ public Tcp()
+ {
+ _tcpClient = new TcpClient();
+ }
+
+ ///
+ /// 通过 一次性配置 TCP 通信参数。
+ ///
+ public Tcp(TcpConfig config) : this()
+ {
+ if (config == null) return;
+ ConfigureDevice(config.IPAddress, config.Port, config.SendTimeout, config.ReceiveTimeout);
+ }
+
+ public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
+ {
+ IPAddress = ipAddress;
+ Port = port;
+ SendTimeout = sendTimeout;
+ ReceiveTimeout = receiveTimeout;
+ }
+
+ public virtual async Task ConnectAsync(CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await ResetConnectionAsync(ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ ///
+ /// 核心内部方法:无锁状态下安全重置并重建连接
+ ///
+ private async Task ResetConnectionAsync(CancellationToken ct)
+ {
+ if (_tcpClient != null)
+ {
+ _tcpClient.Close();
+ _tcpClient.Dispose();
+ }
+
+ _tcpClient = new TcpClient();
+ await _tcpClient.ConnectAsync(IPAddress, Port, ct).ConfigureAwait(false);
+ return true;
+ }
+
+ public virtual void Close()
+ {
+ if (_tcpClient?.Connected == true) _tcpClient.Close();
+ }
+
+ private async Task LoglessSendAsync(byte[] buffer, CancellationToken ct)
+ {
+ if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
+
+ NetworkStream stream = _tcpClient.GetStream();
+ try
+ {
+ if (SendTimeout > 0)
+ {
+ // 使用 WaitAsync 控制发送超时
+ await stream.WriteAsync(buffer, 0, buffer.Length, ct)
+ .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ await stream.WriteAsync(buffer, 0, buffer.Length, ct).ConfigureAwait(false);
+ }
+ }
+ catch (TimeoutException ex)
+ {
+ // 发送超时,安全起见直接断开重建
+ await ResetConnectionAsync(ct);
+ throw new TimeoutException($"TCP 发送数据超时({SendTimeout} ms),连接已重置以确保安全。", ex);
+ }
+ }
+
+ public async Task SendAsync(byte[] buffer, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await LoglessSendAsync(buffer, ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task SendAsync(string str, CancellationToken ct = default)
+ {
+ await SendAsync(Encoding.UTF8.GetBytes(str), ct);
+ }
+
+ public async Task ReadAsync(int length, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
+
+ NetworkStream stream = _tcpClient.GetStream();
+ byte[] buffer = new byte[length];
+ int offset = 0;
+
+ try
+ {
+ while (offset < length)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ // 计算剩余超时时间(这里简单使用配置值,若要求极精准可加入不长计算)
+ int read = ReceiveTimeout > 0
+ ? await stream.ReadAsync(buffer, offset, length - offset, ct)
+ .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct)
+ .ConfigureAwait(false)
+ : await stream.ReadAsync(buffer, offset, length - offset, ct).ConfigureAwait(false);
+
+ if (read == 0) throw new IOException("远程主机已关闭连接");
+ offset += read;
+ }
+ return offset == 0 ? Array.Empty() : buffer[..offset];
+ }
+ catch (TimeoutException ex)
+ {
+ // 关键安全重置:WaitAsync 超时后抛弃老连接,防止未完成的 Read 污染后续 Buffer
+ await ResetConnectionAsync(ct);
+ throw new TimeoutException($"TCP 读取定长数据超时({ReceiveTimeout} ms),连接已重置避免数据错乱。", ex);
+ }
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ private async Task LoglessReadAsync(string delimiter, CancellationToken ct)
+ {
+ if (!IsConnected) throw new InvalidOperationException("TCP未连接。");
+
+ delimiter ??= "\n";
+ var sb = new StringBuilder();
+ byte[] buffer = new byte[1024];
+ NetworkStream stream = _tcpClient.GetStream();
+
+ try
+ {
+ while (true)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ int bytesRead = 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 (bytesRead == 0) throw new IOException("远程主机已关闭连接");
+
+ sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead));
+
+ string currentText = sb.ToString();
+ int index = currentText.IndexOf(delimiter, StringComparison.Ordinal);
+ if (index >= 0)
+ {
+ return currentText.Substring(0, index).Trim();
+ }
+ }
+ }
+ catch (TimeoutException ex)
+ {
+ // 当 SCPI 仪表由于命令发错不响应时,WaitAsync 会立功并抛出 TimeoutException
+ // 此时我们在内部秒速“重置连接”,上层业务只会收到一个干净的超时报错,但由于锁和自动重连,下一次通信直接恢复!
+ await ResetConnectionAsync(ct);
+ throw new TimeoutException($"SCPI 读取超时(未收到结束符 '{delimiter}',等待:{ReceiveTimeout} ms),通信链路已自动刷新。", ex);
+ }
+ }
+
+ public async Task ReadAsync(string delimiter = "\n", CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await LoglessReadAsync(delimiter, ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ public async Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ await LoglessSendAsync(Encoding.UTF8.GetBytes(command), ct);
+ return await LoglessReadAsync(delimiter, ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+ #region 扩展:读取所有可用的二进制网络字节 (无 SCPI 块头解析)
+
+ ///
+ /// 无锁核心方法:发送命令并一次性读取所有回传的二进制原始数据包(不进行 # 协议头解析,专用于读取纯文件流如 PNG)
+ ///
+ private async Task 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);
+ }
+ }
+ }
+
+ ///
+ /// 【公开方法】发送命令并读取设备回传的全部原始二进制字节数组(不带协议头解析,直接返回整个字节缓冲区)
+ ///
+ public async Task ReadAllBytesAsync(string queryCommand, CancellationToken ct = default)
+ {
+ await _commLock.WaitAsync(ct);
+ try
+ {
+ return await LoglessReadAllBytesAsync(queryCommand, ct);
+ }
+ finally
+ {
+ _commLock.Release();
+ }
+ }
+
+ #endregion
+ public void Dispose()
+ {
+ _tcpClient?.Dispose();
+ _commLock?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/DeviceCommand.csproj b/DeviceCommand/DeviceCommand.csproj
new file mode 100644
index 0000000..ca7efa5
--- /dev/null
+++ b/DeviceCommand/DeviceCommand.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeviceCommand/Devices/GantryControlTcp.cs b/DeviceCommand/Devices/GantryControlTcp.cs
new file mode 100644
index 0000000..457a8b3
--- /dev/null
+++ b/DeviceCommand/Devices/GantryControlTcp.cs
@@ -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
+{
+ [ACPCommand]
+ 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位)
+
+ // =================================================================
+ // ------------------ 核心控制逻辑与参数读写 ------------------------
+ // =================================================================
+
+ ///
+ /// 轴使能控制
+ ///
+ /// 轴编号 (1 - 6)
+ 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);
+ }
+
+ ///
+ /// 读取指定轴的使能状态
+ ///
+ /// 1: X轴, 2: Y轴, 3: Z轴
+ public async Task 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];
+ }
+
+ ///
+ /// 同时设置 X/Y/Z 轴的原点位置(带各自独立的范围检查)
+ ///
+ /// X轴原点位置 (范围: 0 ~ 50000)
+ /// Y轴原点位置 (范围: 0 ~ 50000)
+ /// Z轴原点位置 (范围: 0 ~ 20000)
+ 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);
+ }
+
+ ///
+ /// 批量查询当前 X、Y、Z 三轴设定的原点位置值
+ ///
+ /// 返回包含三轴原点位置的元组 (X, Y, Z)
+ 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);
+ }
+
+ ///
+ /// 设置回原点定位速度(带 0 - 100000 范围检查)
+ ///
+ 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);
+ }
+
+ ///
+ /// 查询当前设定的回原点定位速度
+ ///
+ public async Task 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);
+ }
+
+ ///
+ /// 设置指定轴的运行速度(带 0 - 100000 范围检查)
+ ///
+ /// 1: X轴, 2: Y轴, 3: Z轴
+ 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);
+ }
+
+ ///
+ /// 读取指定轴当前设定的运行速度
+ ///
+ public async Task 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);
+ }
+
+ ///
+ /// 写入指定轴的目标位置(动态安全校验:目标位置不能超过 [轴最大物理极限 - 当前轴原点设置值])
+ ///
+ 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);
+ }
+
+ ///
+ /// 读取指定轴当前设定的目标指定位置
+ ///
+ public async Task 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);
+ }
+
+ ///
+ /// 设置系统紧停锁定状态 (true: 锁定激活, false: 解除锁定)
+ ///
+ public async Task SetEStopStateAsync(bool state, CancellationToken ct = default)
+ {
+ await WriteSingleCoilAsync(_slaveAddress, ADDR_M_ESTOP, state, ct);
+ }
+
+ ///
+ /// 设置指定轴的停止控制状态 (true: 持续输出停止信号, false: 释放停止信号)
+ ///
+ 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);
+ }
+
+ ///
+ /// 全局回零(安全校验版:先检查回原点速度是否为 0,为0则拒绝触发)
+ ///
+ 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);
+ }
+
+ ///
+ /// 单轴指定位置启动(含速度判定、使能自动补齐、动作目标位置独立范围校验)
+ ///
+ /// 1: X轴, 2: Y轴, 3: Z轴
+ 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);
+ }
+
+ ///
+ /// 三轴同时启动(全开:严格校验全部运行轴速度、批量检测与补正1~6号轴使能、联动校验三轴目标范围极限)
+ ///
+ 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 内部高低字工具转化方法
+
+ ///
+ /// 触发点动脉冲信号(置 1 后,等待 100ms 自动置 0 释放)
+ ///
+ 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);
+ }
+
+ ///
+ /// 将 32 位整型数据转换为 Modbus 的 2 个 16 位无符号整数(低字在前 CDAB 格式)
+ ///
+ private ushort[] Int32ToUshorts(int value)
+ {
+ ushort lowWord = (ushort)(value & 0xFFFF);
+ ushort highWord = (ushort)((value >> 16) & 0xFFFF);
+ return new ushort[] { lowWord, highWord };
+ }
+
+ ///
+ /// 将 Modbus 的 2 个 16 位无符号整数转换为 32 位整型(低字在前 CDAB 格式)
+ ///
+ private int UshortsToInt32(ushort[] registers)
+ {
+ uint lowWord = registers[0];
+ uint highWord = registers[1];
+ return (int)((highWord << 16) | lowWord);
+ }
+
+ #endregion
+
+ #region 轴绝对位置读取(只读)
+
+ ///
+ /// 读取指定轴的绝对位置(相对原点的实时位置,只读)
+ ///
+ /// 轴编号 (1-6)
+ public async Task 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);
+ }
+
+ ///
+ /// 批量读取 1-6 轴的绝对位置(一次性读取 22 个寄存器,高效)
+ ///
+ /// 返回 6 轴绝对位置元组
+ 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
+ }
+}
diff --git a/DeviceCommand/Devices/IOBoard.cs b/DeviceCommand/Devices/IOBoard.cs
new file mode 100644
index 0000000..d77b570
--- /dev/null
+++ b/DeviceCommand/Devices/IOBoard.cs
@@ -0,0 +1,49 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using NModbus;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ //[ACPCommand]
+ public class IOBoard : ModbusTcp
+ {
+
+ public IOBoard(TcpConfig config) : base(config)
+ {
+
+ }
+
+
+ public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
+ {
+ await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
+ }
+ public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
+ {
+ var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16);
+ for (int i = 0; i < datas.Length; i++)
+ {
+ if (开始地址 + i < 批量写入.Length)
+ {
+ 批量写入[开始地址 + i] = datas[i];
+ }
+ }
+ await Modbus.WriteMultipleCoilsAsync(站号, 0, 批量写入);
+ }
+ public async Task 读输出开关(byte 站号, ushort 开始地址)
+ {
+ await Modbus.ReadCoilsAsync(站号, 开始地址, 1);
+ }
+ public async Task 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量)
+ {
+ return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量);
+ }
+ }
+}
diff --git a/DeviceCommand/Devices/IOBoardGroup.cs b/DeviceCommand/Devices/IOBoardGroup.cs
new file mode 100644
index 0000000..393e7cb
--- /dev/null
+++ b/DeviceCommand/Devices/IOBoardGroup.cs
@@ -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
+{
+ ///
+ /// 系统控制功能类型枚举 (中文)
+ ///
+ public enum 功能动作 : byte
+ {
+ 单相充电,
+ 抛负载,
+ 短路测试,
+ 蜂鸣器报警
+ }
+
+ ///
+ /// 精简后的 IO 端口信息结构体
+ ///
+ 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;
+ }
+ }
+
+ [ACPCommand]
+ 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> IoMapping
+ = new Dictionary>()
+ {
+ { 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 实例不能为空。");
+ }
+
+
+
+ ///
+ /// 执行指定台架的功能动作控制(带高低电平开关及底层安全互锁)
+ ///
+ /// 台架/工位序号 (1 - 8)
+ /// 控制功能类型 (单相充电, 抛负载, 短路测试, 蜂鸣器报警)
+ /// 开关状态 (true: 开启吸合, false: 关闭断开)
+ /// 异步取消令牌
+ 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, 开关状态);
+ }
+
+ ///
+ /// 内部路由辅助方法:根据板卡索引安全获取当前连接的实例对象
+ ///
+ 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}#] 尚未完成初始化,无法建立总线通信。");
+ }
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/IT7800E.cs b/DeviceCommand/Devices/IT7800E.cs
new file mode 100644
index 0000000..ae85451
--- /dev/null
+++ b/DeviceCommand/Devices/IT7800E.cs
@@ -0,0 +1,263 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ #region 枚举定义
+
+ ///
+ /// 电源电气/耦合模式
+ ///
+ public enum PowerCouplingMode
+ {
+ /// 纯交流模式
+ AC,
+ /// 纯直流模式
+ DC,
+ /// 交直流混合模式
+ ACDC,
+ /// 直交流混合模式
+ DCAC
+ }
+
+ ///
+ /// 设备操作运行角色/模式
+ ///
+ public enum DeviceOperationMode
+ {
+ /// 电压源模式(常规源)
+ VOLTage,
+ /// 电子负载模式(放电拉载)
+ LOAD,
+ /// 电流源模式
+ CURRent
+ }
+
+ #endregion
+
+ [ACPCommand]
+ public class IT7800E : Tcp
+ {
+ // 八台产品共用一个,使用换行符 \n (ASCII 0x0A) 作为 SCPI 结束符[cite: 1]
+ private const string ScpiDelimiter = "\n";
+
+ public IT7800E(TcpConfig config) : base(config)
+ {
+ }
+
+ #region 1. IEEE 488.2 公共命令
+
+ public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
+ {
+ await SendAsync($"*CLS{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 重置设备(CancellationToken ct = default)
+ {
+ await SendAsync($"*RST{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ public virtual async Task 读取状态字节(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ #endregion
+
+ #region 2. 耦合模式与拉载开关控制
+
+ ///
+ /// 设置电源输出源的电气模式/工作耦合模式 (AC, DC, ACDC, DCAC)[cite: 1]
+ ///
+ public virtual async Task 设置电源模式(PowerCouplingMode 模式, CancellationToken ct = default)
+ {
+ await SendAsync($":SOURce:FUNCtion {模式}{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ ///
+ /// 查询电源当前的工作电气模式[cite: 1]
+ ///
+ public virtual async Task 查询电源模式(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":SOURce:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
+ {
+ string 参数 = 开启 ? "ON" : "OFF"; //[cite: 1]
+ await SendAsync($":OUTPut {参数}{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ public virtual async Task 查询DC输出状态(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":OUTPut?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ #endregion
+
+ #region 3. 设定输出参数设置 (AC 电压/频率/DC 电压)
+
+ public virtual async Task 设置交流电压(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ public virtual async Task 设置直流电压(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:DC {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ public virtual async Task 设置频率(double 频率, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:FREQuency {0:F2}{1}", 频率, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ #endregion
+
+ #region 4. 测量与数据回测(高频数据轮询核心)
+
+ [Monitorable("交流电源电压")]
+ public virtual async Task 查询实际电压(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ [Monitorable("交流电源电流")]
+ public virtual async Task 查询实际电流(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ [Monitorable("交流电源功率")]
+ public virtual async Task 查询实际功率(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 查询视在功率(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:POWer:APParent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 查询实际频率(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 查询功率因数(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":MEASure:POWer:PFACtor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ #endregion
+
+ #region 5. 保护参数设置与系统监控
+
+ public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent:PROTection:RMS {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:PROTection:PEAK {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
+ await SendAsync(cmd, ct);
+ }
+
+ public virtual async Task 查询错误信息(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ public virtual async Task 切换远程控制模式(CancellationToken ct = default)
+ {
+ await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ public virtual async Task 切换本地控制模式(CancellationToken ct = default)
+ {
+ 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. 充放电与能量统计扩展命令
+
+ ///
+ /// 清除最近的安时(Ah)与瓦时(Wh)统计数据[cite: 1]
+ ///
+ 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]
+ }
+
+ ///
+ /// 读取当前充放电累计的安时值 (单位: Ah)[cite: 1]
+ ///
+ public virtual async Task 查询累计安时_Ah(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"FETCh:AHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ ///
+ /// 读取当前充放电累计的瓦时值 (单位: Wh)[cite: 1]
+ ///
+ public virtual async Task 查询累计瓦时_Wh(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"FETCh:WHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ ///
+ /// 读取充放电积分持续时间 (单位: 秒)[cite: 1]
+ ///
+ public virtual async Task 查询积分时间(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"FETCh:ENERgy:TIME?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ ///
+ /// 切换设备的操作模式 (VOLTage: 电源模式, LOAD: 负载模式/放电, CURRent: 电流源)[cite: 1]
+ ///
+ public virtual async Task 设置操作模式(DeviceOperationMode 模式, CancellationToken ct = default)
+ {
+ await SendAsync($"SYSTem:OPERation:MODE {模式}{ScpiDelimiter}", ct); //[cite: 1]
+ }
+
+ ///
+ /// 设置放电截止电压/电压下限 (单位: V,用于防止电池过放)[cite: 1]
+ ///
+ 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
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/N36200.cs b/DeviceCommand/Devices/N36200.cs
new file mode 100644
index 0000000..0fe6eb4
--- /dev/null
+++ b/DeviceCommand/Devices/N36200.cs
@@ -0,0 +1,386 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ //[ACPCommand]
+ public class N36200 : Tcp
+ {
+ //只有一个,八台产品一起用一个
+ // 手册第 9 页 2.2.4 明确规定:命令结束符为换行符 (ASCII 字符 LF,即 \n)
+ private const string ScpiDelimiter = "\n";
+
+ ///
+ /// 构造函数:传入 一次性初始化 N36200/N36300 设备通信参数。
+ ///
+ public N36200(TcpConfig config) : base(config)
+ {
+ }
+
+ #region 3.1. IEEE 488.2 公共命令
+
+ ///
+ /// 3.1.1. 清除标准事件状态寄存器和错误队列
+ ///
+ public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
+ {
+ await SendAsync($"*CLS{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.1.4. 读取直流电源相关信息(制造商、产品型号、系统 SN、软件版本号)
+ ///
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.1.7. 恢复出厂设置 (注意:设备重置保存数据大约需要 10 秒)
+ ///
+ public virtual async Task 重置设备(CancellationToken ct = default)
+ {
+ await SendAsync($"*RST{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.1.8. 读取状态字节寄存器(只读寄存器,读取时不会清除位)
+ ///
+ public virtual async Task 读取状态字节(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.2. 设定输出电压与限流值
+
+ ///
+ /// 3.2.1. 设定输出电压值 (单位: V)
+ ///
+ public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.2.1. 查询输出电压设定值 (单位: V)
+ ///
+ public virtual async Task 查询电压设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"SOURce:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.2.2. 设置输出限流值 (单位: A)
+ ///
+ public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.2.2. 查询输出限流值设定值 (单位: A)
+ ///
+ public virtual async Task 查询电流设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"SOURce:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.2.3. 设置输出模拟内阻值 (单位: mΩ)
+ ///
+ public virtual async Task 设置内阻(double 内阻, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:INTErnalres {0:F1}{1}", 内阻, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.2.4. 保存当前测试参数到指定存储组 (范围: 1-10)
+ ///
+ public virtual async Task 保存测试参数(int 组别, CancellationToken ct = default)
+ {
+ if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
+ await SendAsync($"SOURce:FUNCtion:SAVe {组别}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.2.5. 调用指定存储组的测试参数 (范围: 1-10)
+ ///
+ public virtual async Task 调用测试参数(int 组别, CancellationToken ct = default)
+ {
+ if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
+ await SendAsync($"SOURce:FUNCtion:RECAll {组别}{ScpiDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 3.3. 输出控制及状态
+
+ ///
+ /// 3.3.1. 控制电源输出开关 (True: 开启, False: 关闭)
+ ///
+ public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
+ {
+ string 参数 = 开启 ? "ON" : "OFF";
+ await SendAsync($"OUTPut:ONOFF {参数}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.3.1. 查询电源输出开关状态 (返回 "ON" 或 "OFF")
+ ///
+ public virtual async Task 查询DC输出开关状态(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"OUTPut:ONOFF?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.3.2. 设定电源工作模式
+ /// (NORMal: 普通模式, CHARge: 电池充电, SEQuence: 序列模式, CPOWer: 恒功率模式, CARWave: 汽车测试, APG: 外部编程)
+ ///
+ public virtual async Task 设置运行模式(string 模式, CancellationToken ct = default)
+ {
+ await SendAsync($"OUTPut:MODE {模式}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.3.2. 查询电源当前运行工作模式
+ ///
+ public virtual async Task 查询运行模式(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"OUTPut:MODE?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.3.11. 设置电源输出 CV/CC 优先权 (CV 或 CC)
+ ///
+ public virtual async Task 设置CVCC优先权(string 优先模式, CancellationToken ct = default)
+ {
+ if (优先模式 != "CV" && 优先模式 != "CC") throw new ArgumentException("优先模式只能为 'CV' 或 'CC'");
+ await SendAsync($"OUTPut:PRIority {优先模式}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.3.12. 获取电源状态字(通过解析返回整数的二进制 Bit 位获取全状态环路及告警)
+ ///
+ public virtual async Task 查询设备状态字(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"OUTPut:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.3.13. 获取电源事件告警状态值 (返回整数通过位定义标识 UVP/OVP/OCP/OPP/OTP)
+ ///
+ public virtual async Task 查询事件告警状态(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"OUTPut:EVENT?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.3.13. 清除当前的告警状态
+ ///
+ public virtual async Task 清除告警(CancellationToken ct = default)
+ {
+ await SendAsync($"OUTPut:EVENT 0{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.3.16. 打开/关闭设备定时关机功能
+ ///
+ public virtual async Task 设置定时关机开关(bool 开启, CancellationToken ct = default)
+ {
+ string 参数 = 开启 ? "ON" : "OFF";
+ await SendAsync($"OUTPut:TIMing:SWITch {参数}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 3.3.17. 设定设备定时关机倒计时时间 (单位: s)
+ ///
+ public virtual async Task 设置定时关机时间(double 秒数, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "OUTPut:TIMing:DWELI {0:F1}{1}", 秒数, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.3.18. 控制泄放电路(Bleeder)开关状态
+ ///
+ public virtual async Task 设置泄放电路开关(bool 开启, CancellationToken ct = default)
+ {
+ string 参数 = 开启 ? "ON" : "OFF";
+ await SendAsync($"OUTPut:DISRes {参数}{ScpiDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 3.4. 读取输出电压电流及功率值 (实时轮询核心)
+
+ ///
+ /// 3.4.1. 回读通道输出端子上的实时测得电压值 (单位: V)
+ ///
+ [Monitorable("高压直流电源电压")]
+ public virtual async Task 查询实际电压(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.4.2. 回读通道输出端子上的实时测得电流值 (单位: A)
+ ///
+ [Monitorable("高压直流电源电流")]
+ public virtual async Task 查询实际电流(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.4.3. 回读通道输出端子上的实时测得功率值 (单位: W)
+ ///
+ [Monitorable("高压直流电源功率")]
+ public virtual async Task 查询实际功率(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.4.4. 回读电池充电模式下当前累计已充入的容量值 (单位: mAh)
+ ///
+ public virtual async Task 查询已充电容量MAH(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:MAH?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.4.5. 获取当前电源的硬件额定电压上限值 (单位: V)
+ ///
+ public virtual async Task 获取设备额定电压(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:VOLTage:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 3.4.6. 获取当前电源的硬件额定电流上限值 (单位: A)
+ ///
+ public virtual async Task 获取设备额定电流(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:CURRent:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.5. 保护功能
+
+ ///
+ /// 3.5.1. 设置欠压保护门限值 (单位: V)
+ ///
+ public virtual async Task 设置过欠压保护_UVP(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:LESS:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.4. 设置过压保护门限值 (单位: V)
+ ///
+ public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:OVER:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.6. 设置硬件硬件过流保护硬指标参数 (单位: A)
+ ///
+ public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.8. 设置硬件过功率保护门限值 (单位: W)
+ ///
+ public virtual async Task 设置过功率保护_OPP(double 功率, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:POWer {0:F3}{1}", 功率, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.12. 设置可调节的用户软输出电压下限值 (单位: V)
+ ///
+ public virtual async Task 设置电压下限(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit:LOW {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.13. 设置可调节的用户软输出电压上限值 (单位: V)
+ ///
+ public virtual async Task 设置电压上限(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.14. 设置可调节的用户软输出电流下限值 (单位: A)
+ ///
+ public virtual async Task 设置电流下限(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit:LOW {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.5.15. 设置可调节的用户软输出电流上限值 (单位: A)
+ ///
+ public virtual async Task 设置电流上限(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ #endregion
+
+ #region 3.6. 恒功率模式配置 (CPOWer)
+
+ ///
+ /// 3.6.1. 设定恒功率工作模式下的限定电压值 (单位: V)
+ ///
+ public virtual async Task 设置恒功率模式电压(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.6.2. 设定恒功率工作模式下的限定电流值 (单位: A)
+ ///
+ public virtual async Task 设置恒功率模式电流(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.6.3. 设定恒功率工作模式下的运行功率目标值 (单位: W)
+ ///
+ public virtual async Task 设置恒功率模式功率(double 功率, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:POWer {0:F3}{1}", 功率, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/N36600.cs b/DeviceCommand/Devices/N36600.cs
new file mode 100644
index 0000000..fbca9e4
--- /dev/null
+++ b/DeviceCommand/Devices/N36600.cs
@@ -0,0 +1,484 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ [ACPCommand]
+ public class N36600 : Tcp
+ {
+ private CancellationTokenSource? _heartbeatCts;
+ private Task? _heartbeatTask;
+ private const int HeartbeatInterval = 3000; // 心跳间隔 3 秒
+
+ public bool IsActive { get; private set; } = false;
+ public int ReConnectionAttempts { get; private set; } = 0;
+ public const int MaxReconnectAttempts = 10;
+
+ // 手册第 4 页明确规定:每条命令后面都要加结束符 0x0A (\n)
+ private const string SCPIDelimiter = "\n";
+
+ public N36600(TcpConfig config) : base(config)
+ {
+ }
+
+ ///
+ /// 建立 TCP 连接,成功后自动激活心跳
+ ///
+ public new async Task ConnectAsync(CancellationToken ct = default)
+ {
+ bool isConnected = await base.ConnectAsync(ct);
+ if (isConnected)
+ {
+ IsActive = true;
+ ReConnectionAttempts = 0;
+ StartHeartbeat();
+ }
+ return isConnected;
+ }
+
+ public override void Close()
+ {
+ StopHeartbeat();
+ base.Close();
+ }
+
+ #region 心跳与断线重连逻辑
+
+ [Browsable(false)]
+ public void StartHeartbeat()
+ {
+ if (_heartbeatTask != null && !_heartbeatTask.IsCompleted)
+ return;
+
+ _heartbeatCts?.Cancel();
+ _heartbeatCts?.Dispose();
+ _heartbeatCts = new CancellationTokenSource();
+
+ _heartbeatTask = Task.Run(() => HeartbeatLoop(_heartbeatCts.Token));
+ }
+
+ [Browsable(false)]
+ public void StopHeartbeat()
+ {
+ IsActive = false;
+ if (_heartbeatCts != null && !_heartbeatCts.IsCancellationRequested)
+ {
+ _heartbeatCts.Cancel();
+ }
+ _heartbeatTask = null;
+ }
+
+ private async Task HeartbeatLoop(CancellationToken ct)
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(HeartbeatInterval, ct);
+ if (ct.IsCancellationRequested) break;
+
+ // 使用公共查询命令发送心跳,确保通道连接正常,且不破坏远程或本地锁定状态
+ await SendAsync($"*IDN?{SCPIDelimiter}", ct);
+ IsActive = true;
+ ReConnectionAttempts = 0;
+ }
+ catch (Exception)
+ {
+ IsActive = false;
+ ReConnectionAttempts++;
+
+ if (ReConnectionAttempts > MaxReconnectAttempts)
+ {
+ StopHeartbeat();
+ base.Close();
+ return;
+ }
+
+ await ReconnectDeviceAsync(ct);
+ }
+ }
+ }
+
+ private async Task ReconnectDeviceAsync(CancellationToken ct)
+ {
+ try
+ {
+ await ConnectAsync(ct);
+ }
+ catch (Exception)
+ {
+ // 静默处理,等待下一轮心跳重试
+ }
+ }
+
+ #endregion
+
+ #region 3.1 IEEE 488.2 公用命令
+
+ ///
+ /// 3.1.1 读取电源的相关信息(制造商, 产品标号, 产品序列号, 软件版本号)
+ ///
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.1.2 从指定的单位中恢复 *SAV 命令保存的设定值 (参数:1~99)
+ ///
+ public virtual async Task 调用存储状态(int 组别, CancellationToken ct = default)
+ {
+ if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
+ await SendAsync($"*RCL {组别}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.1.3 将仪器当前系统状态保存到非易失性内存中 (参数:1~99)
+ ///
+ public virtual async Task 保存当前状态(int 组别, CancellationToken ct = default)
+ {
+ if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
+ await SendAsync($"*SAV {组别}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.1.4 远程控制触发一次
+ ///
+ public virtual async Task 远程触发(CancellationToken ct = default)
+ {
+ await SendAsync($"*TRG{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.1.5 返回状态、采集电压、采集电流 (格式: 状态码,电压值,电流值)
+ ///
+ public virtual async Task 查询全部状态及采样(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*ALL?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.2 APPLy 命令子系统
+
+ ///
+ /// 3.2.1 设置输出电压和输出电流值
+ ///
+ public virtual async Task 快捷设置电压电流(double 电压, double 电流, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,规范应用 APPL 短格式
+ string cmd = string.Format(CultureInfo.InvariantCulture, "APPL {0:F3},{1:F3}{2}", 电压, 电流, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.2.1 查询当前设定的输出电压和电流值
+ ///
+ public virtual async Task 查询快捷电压电流设定(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"APPL?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.5 MEASure 命令子系统
+
+ ///
+ /// 3.5.1 查询通道输出端子上测得的直流电流值 (A)
+ ///
+ [Monitorable("低压直流电源电流")]
+ public virtual async Task 查询实际电流(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 MEAS:CURR?
+ return await WriteReadAsync($"MEAS:CURR?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.5.2 查询通道输出端子上测得的直流功率值 (W)
+ ///
+ [Monitorable("低压直流电源功率")]
+ public virtual async Task 查询实际功率(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 MEAS:POW?
+ return await WriteReadAsync($"MEAS:POW?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
+ ///
+ [Monitorable("低压直流电源电压")]
+ public virtual async Task 查询实际电压(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 MEAS:VOLT?
+ return await WriteReadAsync($"MEAS:VOLT?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.5.4 查询通道输出端子上测得的电压、电流和功率的组合值
+ ///
+ public virtual async Task 查询电压电流功率数组(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+
+ ///
+ /// 3.5.5 查询通道输出端子上测得的输出时间长度
+ ///
+ public virtual async Task 查询总输出时间长度(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"MEAS:TIME:OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.6 OUTPut 命令子系统
+
+ ///
+ /// 3.6.1 设置电源输出开关
+ ///
+ public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用 OUTP 短格式
+ string 状态 = 开启 ? "ON" : "OFF";
+ await SendAsync($"OUTP {状态}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.6.1 查询通道输出状态 (返回 ON 或 OFF)
+ ///
+ public virtual async Task 查询DC输出状态(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.6.2 控制电源输出定时器的开关状态
+ ///
+ public virtual async Task 设置定时器状态(bool 开启, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string 状态 = 开启 ? "ON" : "OFF";
+ await SendAsync($"OUTP:TIME {状态}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.6.3 设定电源输出定时器的时间 (单位: s, 范围: 0.1 ~ 999999.9)
+ ///
+ public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIME:DATA {0:F1}{1}", 秒数, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.6.4 设定电源输出的模式优先权 (CV优先 或 CC优先)
+ ///
+ public virtual async Task 设置输出模式优先(bool 是CV优先, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string 模式 = 是CV优先 ? "CV" : "CC";
+ await SendAsync($"OUTP:MODE {模式}{SCPIDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 3.7 SOURce 命令子系统
+
+ ///
+ /// 3.7.1.1 清除输出电流保护(OCP)电路状态
+ ///
+ public virtual async Task 清除电流保护状态(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"CURR:PROT:CLE{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.7.1.2 使能或禁止当前输出电流保护(OCP)电路
+ ///
+ public virtual async Task 设置电流保护使能(bool 启用, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,将 STATE 改为更常用的 STAT 简写
+ string 状态 = 启用 ? "ON" : "OFF";
+ await SendAsync($"CURR:PROT:STAT {状态}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.7.1.4 设置输出电流保护(OCP)阀值 (A)
+ ///
+ public virtual async Task 设置过流保护值(double 电流, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号。按 NGI 通用指令树,加上层级限制或者直接作用于 CURR:PROT
+ string cmd = string.Format(CultureInfo.InvariantCulture, "CURR:PROT {0:F3}{1}", 电流, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.7.1.7 设置电源的输出电流值 (A)
+ ///
+ public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 CURR 短格式
+ string cmd = string.Format(CultureInfo.InvariantCulture, "CURR {0:F3}{1}", 电流, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.7.2.1 清除输出电压保护(OVP)电路状态
+ ///
+ public virtual async Task 清除电压保护状态(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"VOLT:PROT:CLE{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.7.2.2 使能或禁止当前输出电压保护(OVP)电路
+ ///
+ public virtual async Task 设置电压保护使能(bool 启用, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string 状态 = 启用 ? "ON" : "OFF";
+ await SendAsync($"VOLT:PROT:STAT {状态}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.7.2.4 设置输出电压保护(OVP)阀值 (V)
+ ///
+ public virtual async Task 设置过压保护值(double 电压, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:PROT {0:F3}{1}", 电压, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.7.2.7 设置电源的输出电压值 (V)
+ ///
+ public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 VOLT 短格式
+ string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT {0:F3}{1}", 电压, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.7.2.8 设定电压输出范围的上限电压值 (V)
+ ///
+ public virtual async Task 设置电压上限限制(double 电压, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", 电压, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+ ///
+ /// 3.7.2.9 清除电压上限限制(恢复为设备物理允许的最大电压值)
+ ///
+ public virtual async Task 清除电压上限限制(CancellationToken ct = default)
+ {
+ // 发送 MAXimum 设为最大值以达到“清除限制”的效果
+ await SendAsync($"VOLT:LIMIT MAX{SCPIDelimiter}", ct); // 对应手册 3.7.2.8
+ }
+ ///
+ /// 3.7.3.1 设置输出功率值 (W)
+ ///
+ public virtual async Task 设置功率(double 功率, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号,改用标准 POW 短格式
+ string cmd = string.Format(CultureInfo.InvariantCulture, "POW {0:F3}{1}", 功率, SCPIDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 3.7.3.2 禁止或使能当前恒功率(CP)输出
+ ///
+ public virtual async Task 设置恒功率输出使能(bool 启用, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string 状态 = 启用 ? "ON" : "OFF";
+ await SendAsync($"POW:STAT {状态}{SCPIDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 3.9 SYSTem 命令子系统
+
+ ///
+ /// 3.9.1 控制蜂鸣器开关
+ ///
+ public virtual async Task 设置蜂鸣器状态(bool 开启, CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ string 状态 = 开启 ? "ON" : "OFF";
+ await SendAsync($"SYST:BEEP:STAT {状态}{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.9.2 使蜂鸣器强制鸣叫一声
+ ///
+ public virtual async Task 蜂鸣器鸣叫(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"SYST:BEEP{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.9.4 查询仪器当前出错记录数量 (最大 18 组)
+ ///
+ public virtual async Task 查询错误记录数量(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"SYST:ERR:COUN?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.9.5 读取仪器的出错信息 (成功返回 0,"No error")
+ ///
+ public virtual async Task 查询错误信息(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ return await WriteReadAsync($"SYST:ERR?{SCPIDelimiter}", SCPIDelimiter, ct);
+ }
+
+ ///
+ /// 3.9.7 设置电源为面板控制模式 (本地 Local 状态,前面板按键可用)
+ ///
+ public virtual async Task 切换本地控制模式(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"SYST:LOC{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.9.8 设置电源为远程控制模式 (Remote 状态)
+ ///
+ public virtual async Task 切换远程控制模式(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"SYST:REM{SCPIDelimiter}", ct);
+ }
+
+ ///
+ /// 3.9.9 通过通信接口设置电源为远程控制锁定模式
+ ///
+ public virtual async Task 远程控制模式锁定(CancellationToken ct = default)
+ {
+ // 修正:去除前缀冒号
+ await SendAsync($"SYST:RWL{SCPIDelimiter}", ct);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/N69200.cs b/DeviceCommand/Devices/N69200.cs
new file mode 100644
index 0000000..14dd785
--- /dev/null
+++ b/DeviceCommand/Devices/N69200.cs
@@ -0,0 +1,575 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ [ACPCommand]
+ public class N69200 : Tcp
+ {
+ // 手册第 15 页 2.2.4 明确规定:命令结束符为换行符 (ASCII 字符 LF,即 \n)
+ private const string ScpiDelimiter = "\n";
+
+ ///
+ /// 构造函数:传入 一次性初始化 N69200 电子负载通信参数。
+ ///
+ public N69200(TcpConfig config) : base(config)
+ {
+ }
+
+ #region 3.1. IEEE 488.2 公共命令
+
+ ///
+ /// 清除状态命令。清除标准事件状态寄存器和错误队列 (*CLS)
+ ///
+ public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
+ {
+ await SendAsync($"*CLS{ScpiDelimiter}", ct);
+ }
+ ///
+ /// 查询错误信息
+ ///
+ public virtual async Task 查询错误信息(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+
+ ///
+ /// 读取电子负载设备识别字符串 (*IDN?)
+ ///
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 复位命令。使电子负载恢复到出厂默认配置状态 (*RST)
+ ///
+ public virtual async Task 重置设备(CancellationToken ct = default)
+ {
+ await SendAsync($"*RST{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 读取标准状态字节寄存器 (*STB?)
+ ///
+ public virtual async Task 读取状态字节(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 保存当前系统参数到指定的存储单元 (1~100)
+ ///
+ public virtual async Task 保存当前状态(int 单元, CancellationToken ct = default)
+ {
+ if (单元 < 1 || 单元 > 100)
+ throw new ArgumentOutOfRangeException(nameof(单元), "存储单元位置应在 1~100 之间");
+
+ await SendAsync($"*SAV {单元}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 调用指定存储单元中保存的系统参数 (1~100)
+ ///
+ public virtual async Task 调用存储状态(int 单元, CancellationToken ct = default)
+ {
+ if (单元 < 1 || 单元 > 100)
+ throw new ArgumentOutOfRangeException(nameof(单元), "存储单元位置应在 1~100 之间");
+
+ await SendAsync($"*RCL {单元}{ScpiDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 3.2. 电子负载模式切换及拉载开关
+
+ ///
+ /// 控制电子负载的输入控制开关(INPut:STATe )
+ ///
+ public virtual async Task 设置DC输入(bool 开启, CancellationToken ct = default)
+ {
+ string 参数 = 开启 ? "1" : "0";
+ await SendAsync($"INPut:STATe {参数}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 查询电子负载当前的拉载开关状态 (返回 0 或 1)
+ ///
+ public virtual async Task 查询DC输入状态(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"INPut:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 设置电子负载的基本工作模式 (INPut:FUNCtion )
+ /// (支持 CC, CV, CR, CP, CCD, CVD, CRD, CPD, OCP, OPP, ESR, MPP, SWEEP 等)
+ ///
+ public enum DeviceWorkMode
+ {
+ CC,
+ CV,
+ CR,
+ CP,
+ CCD,
+ 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);
+ }
+
+ ///
+ /// 查询电子负载当前处于何种工作模式
+ ///
+ public virtual async Task 查询负载模式(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"INPut:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.3. 负载参数设置 (CC / CV / CP / CR 定态大量程)
+
+ ///
+ /// 设定恒电流模式(CC)大量程下的拉载电流目标值 (单位: A)
+ ///
+ public virtual async Task 设置恒电流CC(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CC:HIGH:LEVel {0:F4}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 查询恒电流模式大量程下设定的拉载电流值
+ ///
+ public virtual async Task 查询恒电流CC设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"STATic:CC:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 设定恒电压模式(CV)大量程下的拉载电压目标值 (单位: V)
+ ///
+ public virtual async Task 设置恒电压CV(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CV:HIGH:LEVel {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 查询恒电压模式大量程下设定的拉载电压值
+ ///
+ public virtual async Task 查询恒电压CV设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"STATic:CV:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 设定恒功率模式(CP)大量程下的拉载功率目标值 (单位: W)
+ ///
+ public virtual async Task 设置恒功率CP(double 功率, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CP:HIGH:LEVel {0:F3}{1}", 功率, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 查询恒功率模式大量程下设定的拉载功率值
+ ///
+ public virtual async Task 查询恒功率CP设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"STATic:CP:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 设定恒电阻模式(CR)大量程下的等效拉载电阻阻值 (单位: Ω)
+ ///
+ public virtual async Task 设置恒电阻CR(double 电阻, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "STATic:CR:HIGH:LEVel {0:F4}{1}", 电阻, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 查询恒电阻模式大量程下设定的拉载电阻值
+ ///
+ public virtual async Task 查询恒电阻CR设定(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"STATic:CR:HIGH:LEVel?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.4. 测量与数据回测(高频数据轮询核心)
+
+ ///
+ /// 回读电子负载输入端子上的实时测得电压值 (单位: V)
+ ///
+ [Monitorable("高压直流负载电压")]
+ public virtual async Task 查询实际电压(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 回读电子负载输入端子上的实时测得电流值 (单位: A)
+ ///
+ [Monitorable("高压直流负载电流")]
+ public virtual async Task 查询实际电流(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 回读电子负载输入端子上的实时测得功率值 (单位: W)
+ ///
+ [Monitorable("高压直流负载功率")]
+ public virtual async Task 查询实际功率(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 3.5. 保护限制参数设置
+
+ ///
+ /// 设置电子负载的电流软保护阈值 (单位: A, 设置为 0 表示关闭)
+ ///
+ public virtual async Task 设置过流保护值_OCP(double 电流, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:CURRent:PROTection {0:F3}{1}", 电流, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 设置电子负载的功率软保护阈值 (单位: W, 设置为 0 表示关闭)
+ ///
+ public virtual async Task 设置过功率保护值_OPP(double 功率, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:POWer:PROTection {0:F3}{1}", 功率, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 设置电子负载的电压软保护阈值 (单位: V, 设置为 0 表示关闭)
+ ///
+ public virtual async Task 设置过压保护值_OVP(double 电压, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "INPut:VOLTage:PROTection {0:F3}{1}", 电压, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ #endregion
+
+ #region 3.6. 系统控制及报警清除
+
+ ///
+ /// 读取负载报警/事件寄存器信息 (MEASure:EVENT?)
+ ///
+ public virtual async Task 查询报警事件信息(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"MEASure:EVENT?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 清除负载当前的保护触发锁定状态 (INPut:CLearPROTect)
+ ///
+ public virtual async Task 清除保护告警(CancellationToken ct = default)
+ {
+ await SendAsync($"INPut:CLearPROTect{ScpiDelimiter}", ct);
+ }
+
+ #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 查询放电实时时间(CancellationToken ct = default)
+ => await WriteReadAsync($"DISCHarge:ECHO:TIMe?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 查询放电实时容量(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 查询OCP状态(CancellationToken ct = default)
+ => await WriteReadAsync($"OCP:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取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 查询OPP状态(CancellationToken ct = default)
+ => await WriteReadAsync($"OPP:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取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 读取扫频当前运行频率(CancellationToken ct = default)
+ => await WriteReadAsync($"SWEEP:CFRequency?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取扫频测试结果(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 查询ESR状态(CancellationToken ct = default)
+ => await WriteReadAsync($"ESR:STAtus?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取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 查询MPPT状态(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:STATus?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取MPPT最大功率(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:MPOWer?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取MPPT最大功率电压(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:MVOLtage?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取MPPT最大功率电流(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:MCURrent?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取MPPT开路电压(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:OVOLtage?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ public virtual async Task 读取MPPT短路电流(CancellationToken ct = default)
+ => await WriteReadAsync($"MPP:SCURrent?{ScpiDelimiter}", ScpiDelimiter, ct);
+
+ #endregion
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/SDS2000X_HD.cs b/DeviceCommand/Devices/SDS2000X_HD.cs
new file mode 100644
index 0000000..98fed94
--- /dev/null
+++ b/DeviceCommand/Devices/SDS2000X_HD.cs
@@ -0,0 +1,326 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.Globalization;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ [ACPCommand]
+ public class SDS2000X_HD : Tcp
+ {
+ // 示波器底层 Socket 字符串命令以换行符 \n 结束
+ private const string ScpiDelimiter = "\n";
+
+ ///
+ /// 构造函数:传入 一次性初始化示波器通信参数。
+ ///
+ public SDS2000X_HD(TcpConfig config) : base(config)
+ {
+ }
+
+ #region 1. IEEE 488.2 公共命令
+
+ ///
+ /// 清除标准事件状态寄存器和错误队列
+ ///
+ public virtual async Task 清除状态(CancellationToken ct = default)
+ {
+ await SendAsync($"*CLS{ScpiDelimiter}", ct);
+ }
+ ///
+ /// 查询错误信息
+ ///
+ public virtual async Task 查询错误信息(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
+ }
+ ///
+ /// 读取示波器识别字符串(制造商、型号、序列号、固件版本)
+ ///
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 复位命令。使示波器恢复到默认的出厂设置
+ ///
+ public virtual async Task 重置设备(CancellationToken ct = default)
+ {
+ await SendAsync($"*RST{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 查询先前操作是否完成。
+ ///
+ public virtual async Task 检查操作完成_OPC(CancellationToken ct = default)
+ {
+ string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
+ return res.Trim() == "1";
+ }
+
+ #endregion
+
+ #region 2. 运行与捕获控制 (Run / Stop / Single)
+
+ ///
+ /// 控制示波器开始捕获波形
+ ///
+ public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
+ {
+ await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 停止捕获波形
+ ///
+ public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
+ {
+ await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 强制示波器进入单次触发捕获模式
+ ///
+ public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
+ {
+ await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 触发一次波形采样
+ ///
+ public virtual async Task 强制触发(CancellationToken ct = default)
+ {
+ await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
+ }
+ ///
+ /// 示波器触发控制模式(符合 SDS2000X-HD 规范)。
+ ///
+ public enum TriggerMode
+ {
+ ///
+ /// 自动触发模式 (即使无触发信号也周期性刷屏)
+ ///
+ AUTO,
+
+ ///
+ /// 普通触发模式 (仅当满足触发条件时才刷新)
+ ///
+ NORM,
+
+ ///
+ /// 单次触发模式 (捕捉到一次满足条件的信号后立刻 STOP)
+ ///
+ SINGLE
+ }
+ ///
+ /// 设置触发模式 (AUTO 自动, NORM 普通, SINGLE 单次)
+ ///
+ /// 触发模式枚举
+ /// 取消令牌
+ 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
+
+ #region 3. Channel 垂直控制子系统 (C1 ~ C4)
+
+ ///
+ /// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
+ ///
+ public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
+ {
+ string state = enable ? "ON" : "OFF";
+ await SendAsync($"C{channel}:TRAce {state}{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 设置指定通道的垂直电压档位 (Volts/Div)
+ ///
+ public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 查询指定通道当前的电压档位
+ ///
+ public virtual async Task 查询通道电压档位(int channel, CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 设置指定通道的垂直偏移量 (Offset)
+ ///
+ 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);
+ await SendAsync(cmd, ct);
+ }
+ ///
+ /// 示波器通道输入阻抗类型。
+ ///
+ public enum ImpedanceType
+ {
+ /// 50Ω 阻抗
+ FIFty,
+
+ /// 1MΩ 阻抗
+ 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
+
+ #region 4. Timebase 水平时基子系统
+
+ ///
+ /// 设置示波器的水平时基档位 (Time/Div)
+ ///
+ public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 设置示波器的触发水平延迟位置 (Horizontal Delay)
+ ///
+ public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
+ {
+ string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ #endregion
+
+ #region 5. Trigger 触发子系统
+
+ ///
+ /// 设置边沿触发的电平值 (Trigger Level)
+ ///
+ public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
+ {
+
+ string cmd = string.Format(CultureInfo.InvariantCulture, "TRIGger:EDGE:LEVel {0:F3}{1}", level, ScpiDelimiter);
+ await SendAsync(cmd, ct);
+ }
+
+ ///
+ /// 设置边沿触发的触发源 (例如: C1, C2, C3, C4, EX, LINE)
+ ///
+ public virtual async Task 设置触发源(string source, CancellationToken ct = default)
+ {
+ await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct);
+ }
+
+ #endregion
+
+ #region 6. Measure 自动测量参数回读
+
+ ///
+ /// 查询指定通道自动测量项的当前实时测量数值
+ ///
+ public virtual async Task 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
+ {
+ string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
+ return await WriteReadAsync(query, ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 查询指定通道的电压峰峰值 (Vpp)
+ ///
+ public virtual async Task 查询实际电压峰峰值(int channel, CancellationToken ct = default)
+ {
+ return await 查询通道测量项参数(channel, "PKPK", ct);
+ }
+
+ ///
+ /// 查询指定通道的频率值 (Frequency)
+ ///
+ public virtual async Task 查询实际频率(int channel, CancellationToken ct = default)
+ {
+ return await 查询通道测量项参数(channel, "FREQ", ct);
+ }
+
+ ///
+ /// 查询指定通道的真均方根电压值 (Vrms)
+ ///
+ public virtual async Task 查询实际电压均方根(int channel, CancellationToken ct = default)
+ {
+ return await 查询通道测量项参数(channel, "RMS", ct);
+ }
+
+ #endregion
+
+ #region 7. 屏幕截图导出子系统
+ /// 【一键获取并保存截图】
+ /// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
+ ///
+ /// 本地绝对保存路径 (例如: @"D:\Oscilloscope\Screen_01.png")
+ /// 取消令牌
+ /// 返回是否获取并保存成功
+ public virtual async Task 获取屏幕图像并保存(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
+ }
+}
\ No newline at end of file
diff --git a/DeviceCommand/Devices/SPAW7000.cs b/DeviceCommand/Devices/SPAW7000.cs
new file mode 100644
index 0000000..1667eca
--- /dev/null
+++ b/DeviceCommand/Devices/SPAW7000.cs
@@ -0,0 +1,259 @@
+using Common.Attributes;
+using DeviceCommand.Base;
+using Model.Models;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DeviceCommand.Devices
+{
+ [ACPCommand]
+ public class SPAW7000 : Tcp
+ {
+ //只有两个,八台产品公用两两分组各用一个
+ // 根据通用 SCPI 与远宽指令规范,使用换行符 (ASCII 字符 LF,即 \n) 作为标准结束符
+ private const string ScpiDelimiter = "\n";
+
+ ///
+ /// 构造函数:传入 一次性初始化 SPAW7000 功率分析记录仪通信参数。
+ ///
+ public SPAW7000(TcpConfig config) : base(config)
+ {
+ }
+
+ #region 1. IEEE 488.2 公共命令
+
+ ///
+ /// 清除状态命令。清除标准事件状态寄存器和错误队列
+ ///
+ public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
+ {
+ await SendAsync($"*CLS{ScpiDelimiter}", ct);
+ }
+ ///
+ /// 查询错误信息 发hsl居然可以但是手册上没有而且软件上也用不了这个命令
+ ///
+ //public virtual async Task 查询错误信息(CancellationToken ct = default)
+ //{
+ // string query = string.Format(":SYSTem:ERRor?", ScpiDelimiter);
+ // return await WriteReadAsync(query, ScpiDelimiter, ct);
+ //}
+ ///
+ /// 读取功率分析仪识别字符串(制造商、产品型号、系统 SN、软件版本号)
+ ///
+ public virtual async Task 查询设备标识(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ ///
+ /// 复位命令。使功率分析仪恢复到出厂默认配置状态
+ ///
+ public virtual async Task 重置设备(CancellationToken ct = default)
+ {
+ await SendAsync($"*RST{ScpiDelimiter}", ct);
+ }
+
+ ///
+ /// 读取标准状态字节寄存器
+ ///
+ public virtual async Task 读取状态字节(CancellationToken ct = default)
+ {
+ return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
+ }
+
+ #endregion
+
+ #region 2. NUMeric 核心数据测量与轮询 (动态单项读取)
+
+ ///
+ /// 查询指定通道的实时 RMS 电压值 (单位: V)
+ ///
+ public virtual async Task 查询实际电压(int channel, CancellationToken ct = default)
+ {
+ // 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);
+ }
+
+ ///
+ /// 查询指定通道的实时 RMS 电流值 (单位: A)
+ ///
+ public virtual async Task 查询实际电流(int channel, CancellationToken ct = default)
+ {
+ // 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);
+ }
+
+ ///
+ /// 查询指定通道的实时有功功率值 (单位: W)
+ ///
+ public virtual async Task 查询实际功率(int channel, CancellationToken ct = default)
+ {
+ // 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);
+ }
+
+ ///
+ /// 查询指定通道的实时频率值 (单位: Hz)
+ ///
+ public virtual async Task 查询频率(int channel, CancellationToken ct = default)
+ {
+ // 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);
+ }
+
+ ///
+ /// 查询指定通道的功率因数 (Power Factor)
+ ///
+ public virtual async Task 查询功率因数(int channel, CancellationToken ct = default)
+ {
+ // 1. 配置 ITEM1 为指定通道的功率因数 (LAMBda)
+ string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 LAMBda,{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);
+ }
+
+ #endregion
+
+ #region 3. INPut 通道电气参数配置
+ ///