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/LoginModule/Views/LoginView.xaml.cs b/LoginModule/Views/LoginView.xaml.cs new file mode 100644 index 0000000..31db76b --- /dev/null +++ b/LoginModule/Views/LoginView.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 LoginModule.Views +{ + /// + /// LoginView.xaml 的交互逻辑 + /// + public partial class LoginView : UserControl + { + public LoginView() + { + InitializeComponent(); + } + } +} diff --git a/LoginModule/Views/RegisterView.xaml b/LoginModule/Views/RegisterView.xaml new file mode 100644 index 0000000..2ab5f8e --- /dev/null +++ b/LoginModule/Views/RegisterView.xaml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LoginModule/Views/RegisterView.xaml.cs b/LoginModule/Views/RegisterView.xaml.cs new file mode 100644 index 0000000..f8d1138 --- /dev/null +++ b/LoginModule/Views/RegisterView.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 LoginModule.Views +{ + /// + /// RegisterView.xaml 的交互逻辑 + /// + public partial class RegisterView : UserControl + { + public RegisterView() + { + InitializeComponent(); + } + } +} diff --git a/MainModule/MainModule.cs b/MainModule/MainModule.cs new file mode 100644 index 0000000..2180a80 --- /dev/null +++ b/MainModule/MainModule.cs @@ -0,0 +1,25 @@ +using DeviceCommand.Base; +using DeviceCommand.Devices; +using MainModule.Views; +using System.Reflection; +using UIShare.GlobalVariable; + +namespace MainModule +{ + [Module(OnDemand=true)] + public class MainModule : IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + IRegionManager regionManager = containerProvider.Resolve(); + regionManager.RegisterViewWithRegion("ShellViewManager", typeof(MainView)); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + containerRegistry.RegisterForNavigation("MainView"); + containerRegistry.RegisterForNavigation("AutomatedTestingView"); + containerRegistry.RegisterForNavigation("ProtocolStartView"); + } + } +} diff --git a/MainModule/MainModule.csproj b/MainModule/MainModule.csproj new file mode 100644 index 0000000..8b229f9 --- /dev/null +++ b/MainModule/MainModule.csproj @@ -0,0 +1,15 @@ + + + + net8.0-windows + enable + true + enable + + + + + + + + diff --git a/MainModule/ViewModels/AutomatedTestingViewModel.cs b/MainModule/ViewModels/AutomatedTestingViewModel.cs new file mode 100644 index 0000000..21a64bb --- /dev/null +++ b/MainModule/ViewModels/AutomatedTestingViewModel.cs @@ -0,0 +1,302 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Windows.Input; +using DeviceCommand.Devices; +using Logger; +using Prism.Ioc; +using TestingModule.ViewModels; +using UIShare; +using UIShare.GlobalVariable; +using UIShare.PubEvent; +using UIShare.UIViewModel; +using UIShare.ViewModelBase; +using static System.Formats.Asn1.AsnWriter; + +namespace MainModule.ViewModels +{ + public class AutomatedTestingViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable + { + #region 私有字段 + private string _testStatus; + private readonly IScopedProvider _scope; + private bool IsInitialized =false; + private bool _isRunningErrorSteps = false; + private SubscriptionToken? _alarmToken; + #endregion + + #region 属性 + public bool KeepAlive => true; // 保持存活 + + public string TestStatus + { + get => _testStatus; + set => SetProperty(ref _testStatus, value); + } + + // 该 AutomatedTestingView 实例独占的 ScopedContext,5 个子面板共享 + public ScopedContext _scopedContext { get; } + public StepRunning _stepRunning { get; } + public GlobalInfo _globalInfo { get; } + public SystemConfig _systemConfig { get; set; } + public DeviceManager _deviceManager { get; set; } + + // 5 个子 ViewModel 全部从同一个 scope 解析,自动注入同一个 ScopedContext + public CommandTreeViewModel CommandTreeVM { get; } + public StepsManagerViewModel StepsManagerVM { get; } + public SingleStepEditViewModel SingleStepEditVM { get; } + public LogAreaViewModel LogAreaVM { get; } + public ParametersManagerViewModel ParametersManagerVM { get; } + #endregion + + public ICommand RefreshCommand { get; set; } + public ICommand BackToProtocolCommand { get; set; } + public ICommand LoadedCommand { get; set; } + + public AutomatedTestingViewModel(IContainerExtension container) : base(container) + { + // 每个 AutomatedTestingViewModel 实例创建独立的容器作用域 + _scope = container.CreateScope(); + _globalInfo =container.Resolve(); + _systemConfig = _scope.Resolve(); + //加载Json数据 + if (ConfigService.IsExit(_globalInfo.CurrentOpeningScope)) + { + string filePath = System.IO.Path.Combine(_systemConfig.SystemPath, $"{_globalInfo.CurrentOpeningScope}.json"); + if (System.IO.File.Exists(filePath)) + { + string json = System.IO.File.ReadAllText(filePath); + Newtonsoft.Json.JsonConvert.PopulateObject(json, _systemConfig); + } + } + //容器解析顺序不要改变!!! + _scopedContext = _scope.Resolve(); + _deviceManager = _scope.Resolve(); + _stepRunning = _scope.Resolve(); + // 从同一个 _scope 解析 5 个子 VM,DI 会把同一个 ScopedContext 注入它们 + CommandTreeVM = _scope.Resolve(); + StepsManagerVM = _scope.Resolve(); + SingleStepEditVM = _scope.Resolve(); + LogAreaVM = _scope.Resolve(); + ParametersManagerVM = _scope.Resolve(); + RefreshCommand = new DelegateCommand(OnRefresh); + BackToProtocolCommand = new DelegateCommand(OnBackToProtocol); + LoadedCommand = new AsyncDelegateCommand(OnLoad); + _eventAggregator.GetEvent().Subscribe(async (scope) => await SilenceBuzzer(scope)); + _alarmToken = _eventAggregator.GetEvent().Subscribe(OnAlarmTriggered); + } + + private async Task SilenceBuzzer(string scope) + { + if (_deviceManager.IOGroup == null) return; + + if (scope == "default") + { + for(int i = 1; i <= 8; i++) + { + await _deviceManager.IOGroup.执行台架动作Async(i, 功能动作.蜂鸣器报警, false); + } + } + else if (scope == TestStatus) + { + int 台架号 = _systemConfig.SharedParameterList + .FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0; + if (台架号 >= 1 && 台架号 <= 8) + { + await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, false); + } + } + } + + /// + /// AlarmEvent 回调: + /// 上下限 → 蜂鸣器报警 true + /// 上下极限 → 蜂鸣器报警 true + 执行对应 scope 的异常流程 + /// + private async void OnAlarmTriggered((string Scope, string Fingerprint, string Status) args) + { + if (args.Scope != TestStatus) return; + if (_deviceManager?.IOGroup == null) return; + + int 台架号 = _systemConfig.SharedParameterList + .FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0; + if (台架号 < 1 || 台架号 > 8) return; + + // 上下限 / 上下极限 均触发蜂鸣器报警 + await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, true); + LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。"); + + // 上下极限额外触发异常流程 + if (args.Status == "超上极限" || args.Status == "超下极限") + { + _ = TriggerErrorStepsAsync(); + } + } + + /// + /// 执行异常流程(带并发保护,同一时间仅允许一次)。 + /// 如果正常流程正在运行,先取消并等待其停止,再执行异常流程。 + /// + private async Task TriggerErrorStepsAsync() + { + if (_isRunningErrorSteps) return; + _isRunningErrorSteps = true; + try + { + // 1. 如果正常流程正在运行,先停止它 + // IsStop == false 表示正在运行;IsStop == null 表示未启动或已结束 + if (_scopedContext.IsStop == false) + { + LoggerHelper.WarnWithNotify(TestStatus, $"[{TestStatus}] 报警触发,正在停止正常流程..."); + _stepRunning.stepCTS.Cancel(); + + // 等待 ShellViewModel 清理完毕(IsStop 变回 null、stepCTS 被重置) + // ExecuteMethodStep 内部硬编码使用 stepCTS.Token,必须等它重置后才能跑异常流程 + var deadline = DateTime.Now.AddSeconds(5); + while ((_scopedContext.IsStop != null || _stepRunning.stepCTS.IsCancellationRequested) + && DateTime.Now < deadline) + { + await Task.Delay(50); + } + + // 超时后仍未重置则强制重置,确保异常流程的设备命令能拿到有效 Token + if (_stepRunning.stepCTS.IsCancellationRequested) + { + _stepRunning.stepCTS = new CancellationTokenSource(); + _scopedContext.IsStop = null; + _scopedContext.RunState = "运行"; + } + + LoggerHelper.WarnWithNotify(TestStatus, $"[{TestStatus}] 正常流程已停止,开始执行异常流程"); + } + + // 2. 执行异常流程 + await _stepRunning.ExecuteErrorSteps( + _scopedContext.Program, + cancellationToken: _stepRunning.errorStepCTS.Token); + } + catch (Exception ex) + { + LoggerHelper.ErrorWithNotify(TestStatus, $"[{TestStatus}] 报警触发异常流程失败: {ex.Message}"); + } + finally + { + _isRunningErrorSteps = false; + if (_stepRunning.errorStepCTS.IsCancellationRequested) + _stepRunning.errorStepCTS = new CancellationTokenSource(); + } + } + + public void Dispose() + { + // 1. 如果 TestStatus 为空,说明还没走到 OnNavigatedTo 赋值,直接释放 scope 即可 + if (string.IsNullOrEmpty(TestStatus)) + { + _scope?.Dispose(); + return; + } + + try + { + if (_alarmToken != null) + _eventAggregator.GetEvent().Unsubscribe(_alarmToken); + _eventAggregator.GetEvent().Unsubscribe(async (scope) => await SilenceBuzzer(scope)); + // 2. 显式释放硬件资源(防止端口占用/死锁) + if (_deviceManager is IDisposable disposableDevice) + { + disposableDevice.Dispose(); + } + (CommandTreeVM as IDisposable)?.Dispose(); + (StepsManagerVM as IDisposable)?.Dispose(); + (SingleStepEditVM as IDisposable)?.Dispose(); + (LogAreaVM as IDisposable)?.Dispose(); + (ParametersManagerVM as IDisposable)?.Dispose(); + + _globalInfo.ContextDic?.Remove(TestStatus); + _globalInfo.StepRunningDic?.Remove(TestStatus); + _globalInfo.ConfigDic?.Remove(TestStatus); + _globalInfo.ScopeDic?.Remove(TestStatus); + } + catch (Exception ex) + { + Logger.LoggerHelper.ErrorWithNotify(TestStatus, $"卸载机台 [{TestStatus}] 全局引用或资源失败: {ex.Message}"); + } + finally + { + _scope?.Dispose(); + } + } + #region 命令处理与事件 + private async Task OnLoad() + { + if (!IsInitialized) + { + await _deviceManager.ConnectAllDevices(); + IsInitialized = true; + } + } + + private void OnRefresh() + { + // 双击:把自己的名字扔出去 + _globalInfo.CurrentScope = TestStatus; + _eventAggregator.GetEvent().Publish(TestStatus); + + } + + private void OnBackToProtocol() + { + // 返回:扔个空字符串出去 + _eventAggregator.GetEvent().Publish(""); + } + #endregion + + #region 重写 + public override void OnNavigatedTo(NavigationContext navigationContext) + { + base.OnNavigatedTo(navigationContext); + if (navigationContext.Parameters.ContainsKey("Name")) + { + TestStatus = navigationContext.Parameters.GetValue("Name"); + _globalInfo.ContextDic.Add(TestStatus, _scopedContext); + _globalInfo.StepRunningDic.Add(TestStatus, _stepRunning); + _globalInfo.ScopeDic.Add(TestStatus, _scope); + _globalInfo.ConfigDic.Add(TestStatus, _systemConfig); + if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath)) + { + var filePath = _systemConfig.DefaultProgramFilePath; + _systemConfig.CurrentACPFile= _systemConfig.DefaultProgramFilePath; + // 读取 JSON 文件 + string json = File.ReadAllText(filePath); + + // 反序列化为 ProgramVM + var program = Newtonsoft.Json.JsonConvert.DeserializeObject(json); + + if (program == null) + { + LoggerHelper.WarnWithNotify(_globalInfo.CurrentScope, $"文件格式不正确或为空: {filePath}"); + return; + } + + // 💡 2. 严格赋值给快照锁定下的当前工位上下文,实现数据完全隔离 + _scopedContext.Program.Parameters = program.Parameters; + _scopedContext.Program.StepCollection = program.StepCollection; + _scopedContext.Program.ErrorStepCollection = program.ErrorStepCollection; + _scopedContext.CurrentFilePath = filePath; + foreach (var item in _systemConfig.SharedParameterList) + { + var parameter = _scopedContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName); + + if (parameter != null) + { + parameter.Value = item.Value; + } + } + } + } + + } + #endregion + } +} \ No newline at end of file diff --git a/MainModule/ViewModels/MainViewModel.cs b/MainModule/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..db8d819 --- /dev/null +++ b/MainModule/ViewModels/MainViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Windows; +using System.Windows.Input; +using UIShare.PubEvent; +using MainModule.ViewModels; +using UIShare.ViewModelBase; + + +namespace MainModule.ViewModels +{ + public class MainViewModel : NavigateViewModelBase, IRegionMemberLifetime + { + #region 私有字段 + private bool IsInitialized = false; + private string _expandedCellName = string.Empty; + #endregion + + #region 属性 + public bool KeepAlive => true; // 保持存活 + + public string ExpandedCellName + { + get => _expandedCellName; + set => SetProperty(ref _expandedCellName, value); + } + #endregion + + #region 命令 + public ICommand LoadedCommand { get; set; } + #endregion + + public MainViewModel(IContainerProvider containerProvider) : base(containerProvider) + { + LoadedCommand = new DelegateCommand(OnLoaded); + _eventAggregator.GetEvent().Subscribe(OnCellExpandRequested); + } + + #region 命令处理与事件 + private void OnLoaded() + { + if (IsInitialized) return; + for (int i = 1; i <= 9; i++) + { + var parameters = new NavigationParameters { { "Name", $"TestCell{i}" } }; + _regionManager.RequestNavigate($"TestCell{i}", "ProtocolStartView", parameters); + } + IsInitialized = true; + } + + // 不再物理搬迁视图,只需修改一个字符串属性 ExpandedCellName, + // XAML 里每个单元的 Style.Triggers 会根据该值处理隐藏 / 跨越 3x3。 + private void OnCellExpandRequested(string cellName) + { + ExpandedCellName = cellName ?? string.Empty; + } + #endregion + + #region 重写 + public override bool IsNavigationTarget(NavigationContext navigationContext) + { + // 之前帮你改好的单例复用逻辑(防止重复 new 和初始化视图) + return true; + } + #endregion + } +} \ No newline at end of file diff --git a/MainModule/ViewModels/ProtocolStartViewModel.cs b/MainModule/ViewModels/ProtocolStartViewModel.cs new file mode 100644 index 0000000..259505b --- /dev/null +++ b/MainModule/ViewModels/ProtocolStartViewModel.cs @@ -0,0 +1,107 @@ +using Prism.Navigation.Regions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using UIShare.GlobalVariable; +using UIShare.ViewModelBase; + +namespace MainModule.ViewModels +{ + public class ProtocolStartViewModel : NavigateViewModelBase + { + #region 私有字段 + private string _testStatus; + private string _moduleColor; + private GlobalInfo _globalInfo; + #endregion + + #region 属性 + public string TestStatus + { + get => _testStatus; + set => SetProperty(ref _testStatus, value); + } + + + #endregion + + #region 命令 + public DelegateCommand StartProtocolCommand { get; } + #endregion + + public ProtocolStartViewModel(IContainerProvider containerProvider) : base(containerProvider) + { + _globalInfo = containerProvider.Resolve(); + StartProtocolCommand = new DelegateCommand(OnStart); + } + + #region 命令处理与事件 + private void OnStart() + { + // 只在当前格子所属的 Cell Region 内完成切换,不会影响其他格子位置 + SwitchNavigate("AutomatedTestingView"); + } + + /// + /// 定位当前视图所在的 Cell Region(TestCell1..TestCell9), + /// 在同一个 Region 内完成跳转。Region 位置由 XAML Grid 锁定,永远不会错位。 + /// + public void SwitchNavigate(string viewName) + { + // 1. 反向查找:哪个 Cell Region 当前托着“我”这个 ProtocolStartView + for (int i = 1; i <= 9; i++) + { + var regionName = $"TestCell{i}"; + if (!_regionManager.Regions.ContainsRegionWithName(regionName)) continue; + + var region = _regionManager.Regions[regionName]; + var myView = region.Views + .OfType() + .FirstOrDefault(v => v.DataContext == this); + + if (myView == null) continue; + + // 2. 透传名称与颜色参数,使 AutomatedTestingViewModel 能正确初始化 + var parameters = new NavigationParameters(); + parameters.Add("Name", TestStatus); + _globalInfo.CurrentOpeningScope = TestStatus; + // 3. 在本格子 Region 内请求导航,导航成功后再移除旧的 ProtocolStartView 以释放资源 + _regionManager.RequestNavigate(regionName, viewName, navResult => + { + if (navResult.Success == true) + { + region.Remove(myView); + } + }, parameters); + _moduleManager.LoadModule("MonitorModule"); + _moduleManager.LoadModule("SettingModule"); + return; + } + } + #endregion + + #region 重写 + public override bool IsNavigationTarget(NavigationContext navigationContext) + { + if (navigationContext.Parameters.ContainsKey("Name")) + return TestStatus == navigationContext.Parameters.GetValue("Name"); + return true; + } + + public override void OnNavigatedTo(NavigationContext navigationContext) + { + base.OnNavigatedTo(navigationContext); + + // 接收导航传参:名称与颜色 + if (navigationContext.Parameters.ContainsKey("Name")) + TestStatus = navigationContext.Parameters.GetValue("Name"); + + + } + #endregion + } +} \ No newline at end of file diff --git a/MainModule/Views/AutomatedTestingView.xaml b/MainModule/Views/AutomatedTestingView.xaml new file mode 100644 index 0000000..2ec97a5 --- /dev/null +++ b/MainModule/Views/AutomatedTestingView.xaml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MainModule/Views/AutomatedTestingView.xaml.cs b/MainModule/Views/AutomatedTestingView.xaml.cs new file mode 100644 index 0000000..3d56642 --- /dev/null +++ b/MainModule/Views/AutomatedTestingView.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 MainModule.Views +{ + /// + /// AutomatedTestingView.xaml 的交互逻辑 + /// + public partial class AutomatedTestingView : UserControl + { + public AutomatedTestingView() + { + InitializeComponent(); + } + } +} diff --git a/MainModule/Views/MainView.xaml b/MainModule/Views/MainView.xaml new file mode 100644 index 0000000..2ad346d --- /dev/null +++ b/MainModule/Views/MainView.xaml @@ -0,0 +1,258 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MainModule/Views/MainView.xaml.cs b/MainModule/Views/MainView.xaml.cs new file mode 100644 index 0000000..f8052f9 --- /dev/null +++ b/MainModule/Views/MainView.xaml.cs @@ -0,0 +1,19 @@ +using MainModule.ViewModels; +using System.Windows; +using System.Windows.Controls; + +namespace MainModule.Views +{ + /// + /// MainView.xaml 的交互逻辑 + /// + public partial class MainView : UserControl + { + public MainView() + { + InitializeComponent(); + } + + + } +} diff --git a/MainModule/Views/ProtocolStartView.xaml b/MainModule/Views/ProtocolStartView.xaml new file mode 100644 index 0000000..d3362bc --- /dev/null +++ b/MainModule/Views/ProtocolStartView.xaml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MainModule/Views/ProtocolStartView.xaml.cs b/MainModule/Views/ProtocolStartView.xaml.cs new file mode 100644 index 0000000..2dca76e --- /dev/null +++ b/MainModule/Views/ProtocolStartView.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 MainModule.Views +{ + /// + /// ProtocolStartView.xaml 的交互逻辑 + /// + public partial class ProtocolStartView : UserControl + { + public ProtocolStartView() + { + InitializeComponent(); + } + } +} diff --git a/Model/Entity/BaseEntity.cs b/Model/Entity/BaseEntity.cs new file mode 100644 index 0000000..8f6f7b0 --- /dev/null +++ b/Model/Entity/BaseEntity.cs @@ -0,0 +1,27 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Entity +{ + public abstract class BaseEntity + { + /// + /// 主键Id + /// + [SugarColumn(ColumnName = "id", ColumnDescription = "主键Id", IsPrimaryKey = true, CreateTableFieldSort = 0)] + public virtual long Id { get; set; } + + /// + /// 删除状态 + /// + [SugarColumn(ColumnName = "IsDel", ColumnDescription = "删除状态(0、未删除;1、已删除)", ColumnDataType = "tinyint", DefaultValue = "0", CreateTableFieldSort = 106)] + public virtual byte IsDel { get; set; } + + [SugarColumn(ColumnName = "CreateTime")] + public DateTime CreateTime { get; set; } + } +} diff --git a/Model/Entity/MonitorValueEntity.cs b/Model/Entity/MonitorValueEntity.cs new file mode 100644 index 0000000..fbb6e35 --- /dev/null +++ b/Model/Entity/MonitorValueEntity.cs @@ -0,0 +1,24 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Entity +{ + public class MonitorValueEntity:BaseEntity + { + /// 监测项名称 + [SugarColumn(ColumnName = "MonitorName")] + public string MonitorName { get; set; } = string.Empty; + + /// 监测值 + [SugarColumn(ColumnName = "MonitorValue")] + public double MonitorValue { get; set; } + + /// 作用域标识(台架名称) + [SugarColumn(ColumnName = "Scope")] + public string Scope { get; set; } = string.Empty; + } +} diff --git a/Model/Entity/TestReportEntity.cs b/Model/Entity/TestReportEntity.cs new file mode 100644 index 0000000..bf833d8 --- /dev/null +++ b/Model/Entity/TestReportEntity.cs @@ -0,0 +1,67 @@ +using SqlSugar; +using System; + +namespace Model.Entity +{ + /// + /// 测试报告记录:每次 ExecuteSteps / ExecuteErrorSteps 中每个步骤的执行结果。 + /// 同一次运行的所有步骤共享同一个 TestRoundId。 + /// + public class TestReportEntity : BaseEntity + { + /// 同一次运行的统一标识(StepRunning.TestRoundID) + [SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")] + public Guid TestRoundId { get; set; } + + /// 作用域/台架名称 + [SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")] + public string Scope { get; set; } = string.Empty; + /// 测试项名称(当前ACP文件路径) + [SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")] + public string FileName { get; set; } = string.Empty; + + /// 步骤序号 + [SugarColumn(ColumnName = "StepIndex")] + public int StepIndex { get; set; } + + /// 步骤名称 + [SugarColumn(ColumnName = "StepName", Length = 200)] + public string StepName { get; set; } = string.Empty; + + /// 步骤类型(普通步骤/循环开始/循环结束) + [SugarColumn(ColumnName = "StepType", Length = 50)] + public string StepType { get; set; } = string.Empty; + + /// 执行的方法名称 + [SugarColumn(ColumnName = "MethodName", Length = 200, IsNullable = true)] + public string? MethodName { get; set; } + + /// 方法的完整类型名(实际方法所属类) + [SugarColumn(ColumnName = "MethodFullName", Length = 500, IsNullable = true)] + public string? MethodFullName { get; set; } + + /// 执行结果(成功/失败/未执行/跳过) + [SugarColumn(ColumnName = "Result", Length = 20)] + public string Result { get; set; } = string.Empty; + + /// 执行耗时(毫秒) + [SugarColumn(ColumnName = "RunTimeMs", IsNullable = true)] + public int? RunTimeMs { get; set; } + + /// 输出值(如果有) + [SugarColumn(ColumnName = "OutputValue", Length = 500, IsNullable = true)] + public string? OutputValue { get; set; } + + /// 子程序嵌套深度(0=主程序) + [SugarColumn(ColumnName = "Depth")] + public int Depth { get; set; } + + /// 是否异常流程步骤 + [SugarColumn(ColumnName = "IsErrorStep")] + public bool IsErrorStep { get; set; } + + /// 循环剩余次数(仅循环步骤有值) + [SugarColumn(ColumnName = "LoopRemaining", IsNullable = true)] + public int? LoopRemaining { get; set; } + } +} diff --git a/Model/Model.csproj b/Model/Model.csproj new file mode 100644 index 0000000..531df3d --- /dev/null +++ b/Model/Model.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Model/Models/AvailableMethodItem.cs b/Model/Models/AvailableMethodItem.cs new file mode 100644 index 0000000..4792397 --- /dev/null +++ b/Model/Models/AvailableMethodItem.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Models +{ + /// + /// 可用设备方法项(供用户选择添加为监测通道) + /// + public class AvailableMethodItem + { + public string DeviceName { get; set; } = string.Empty; + /// 硬件指纹(物理设备唯一标识) + public string Fingerprint { get; set; } = string.Empty; + public string MethodName { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public MethodInfo MethodInfo { get; set; } = null!; + public object Device { get; set; } = null!; + } +} diff --git a/Model/Models/CanMessageShow.cs b/Model/Models/CanMessageShow.cs new file mode 100644 index 0000000..8fa34dd --- /dev/null +++ b/Model/Models/CanMessageShow.cs @@ -0,0 +1,24 @@ +namespace Model.Models +{ + /// + /// CAN 报文展示纯数据类(对应 UIShare.UIViewModel.CanMessageShowModel) + /// + public class CanMessageShow + { + public byte 通道 { get; set; } + + public int 报文ID { get; set; } + + public ulong 时间戳 { get; set; } + + public byte 长度 { get; set; } + + public bool FD { get; set; } + + public bool IsTx { get; set; } + + public byte[] Bytes { get; set; } = new byte[64]; + + public string? 报文 { get; set; } + } +} diff --git a/Model/Models/DeviceInfo.cs b/Model/Models/DeviceInfo.cs new file mode 100644 index 0000000..7e2c415 --- /dev/null +++ b/Model/Models/DeviceInfo.cs @@ -0,0 +1,18 @@ +namespace Model.Models +{ + /// + /// 设备信息纯数据类(对应 UIShare.UIViewModel.DeviceInfoModel) + /// + public class DeviceInfo + { + public string? DeviceName { get; set; } + + public string? DeviceType { get; set; } + + public string? Remark { get; set; } + + public bool IsEnabled { get; set; } + + public bool IsConnected { get; set; } + } +} diff --git a/Model/Models/HardwareReportArgs.cs b/Model/Models/HardwareReportArgs.cs new file mode 100644 index 0000000..420fd88 --- /dev/null +++ b/Model/Models/HardwareReportArgs.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Models +{ + public class HardwareReportArgs + { + /// 作用域标识(台架名称) + public string Scope { get; set; } = string.Empty; + + /// 硬件指纹(物理设备唯一标识,如 "Tcp:192.168.1.1:5000") + public string HardwareFingerprint { get; set; } = string.Empty; + + /// 方法名 + public string MethodName { get; set; } = string.Empty; + + /// 采样值 + public double Value { get; set; } + + /// 采样时间 + public DateTime Time { get; set; } = DateTime.Now; + } +} diff --git a/Model/Models/InstructionNode.cs b/Model/Models/InstructionNode.cs new file mode 100644 index 0000000..bd46445 --- /dev/null +++ b/Model/Models/InstructionNode.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Model.Models +{ + /// + /// 指令节点纯数据类(对应 UIShare.UIViewModel.InstructionNode) + /// + public class InstructionNode + { + public string? Name { get; set; } + + public IList Children { get; set; } = new List(); + + public object? Tag { get; set; } + } +} diff --git a/Model/Models/Method.cs b/Model/Models/Method.cs new file mode 100644 index 0000000..350cbc3 --- /dev/null +++ b/Model/Models/Method.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Model.Models +{ + /// + /// 方法纯数据类(对应 UIShare.UIViewModel.MethodModel) + /// + public class Method + { + public string? Name { get; set; } + + public string? FullName { get; set; } + + public IList Parameters { get; set; } = new List(); + } +} diff --git a/Model/Models/Parameter.cs b/Model/Models/Parameter.cs new file mode 100644 index 0000000..24a45c3 --- /dev/null +++ b/Model/Models/Parameter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Model.Models +{ + /// + /// 参数纯数据类(对应 UIShare.UIViewModel.ParameterModel) + /// + public class Parameter + { + public Guid ID { get; set; } = Guid.NewGuid(); + + public bool IsVisible { get; set; } = true; + + public string? Name { get; set; } + + /// + /// 类型全名(对应 ParameterModel.Type 的 FullName) + /// + public string? TypeName { get; set; } + + /// + /// 参数类别(Input / Output / Temp) + /// + public string? Category { get; set; } + + + public object? Value { get; set; } + + public object? LowerLimit { get; set; } + + public object? UpperLimit { get; set; } + + public bool Result { get; set; } = true; + + public bool IsUseVar { get; set; } + + + public string? VariableName { get; set; } + + public Guid? VariableID { get; set; } + } +} diff --git a/Model/Models/Program.cs b/Model/Models/Program.cs new file mode 100644 index 0000000..5aef8a4 --- /dev/null +++ b/Model/Models/Program.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Model.Models +{ + /// + /// 程序纯数据类(对应 UIShare.UIViewModel.ProgramModel) + /// + public class Program + { + public Guid ID { get; set; } = Guid.NewGuid(); + + public IList StepCollection { get; set; } = new List(); + + public IList ErrorStepCollection { get; set; } = new List(); + + public IList Parameters { get; set; } = new List(); + } +} diff --git a/Model/Models/SerialPortConfig.cs b/Model/Models/SerialPortConfig.cs new file mode 100644 index 0000000..2ef0d87 --- /dev/null +++ b/Model/Models/SerialPortConfig.cs @@ -0,0 +1,27 @@ +using System.IO.Ports; + +namespace Model.Models +{ + /// + /// 串口通信参数(DeviceCommand 内部纯数据类,供设备类构造函数使用)。 + /// 与 UIShare.UIViewModel.SerialPortConfigVM 字段一一对应, + /// StopBits / Parity 在此处使用 System.IO.Ports 强类型枚举, + /// 由 DeviceManager 从字符串解析后填入。 + /// + public class SerialPortConfig + { + 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; + } +} diff --git a/Model/Models/Step.cs b/Model/Models/Step.cs new file mode 100644 index 0000000..812ef6a --- /dev/null +++ b/Model/Models/Step.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace Model.Models +{ + /// + /// 步骤纯数据类(对应 UIShare.UIViewModel.StepModel) + /// + public class Step + { + public Guid ID { get; set; } = Guid.NewGuid(); + + public bool IsUsed { get; set; } = true; + + public int Index { get; set; } + + public string? Name { get; set; } + + public string? StepType { get; set; } + + public Method? Method { get; set; } + + public Program? SubProgram { get; set; } + + public int? LoopCount { get; set; } + + public int? CurrentLoopCount { get; set; } + + public Guid? LoopStartStepId { get; set; } + + public int Result { get; set; } = -1; + + public int? RunTime { get; set; } + + public string? OKExpression { get; set; } + + public string GotoSettingString { get; set; } = ""; + + public Guid? OKGotoStepID { get; set; } + + public Guid? NGGotoStepID { get; set; } + + public string? Description { get; set; } + } +} diff --git a/Model/Models/SubProgramItem.cs b/Model/Models/SubProgramItem.cs new file mode 100644 index 0000000..038b47d --- /dev/null +++ b/Model/Models/SubProgramItem.cs @@ -0,0 +1,12 @@ +namespace Model.Models +{ + /// + /// 子程序项纯数据类(对应 UIShare.UIViewModel.SubProgramItem) + /// + public class SubProgramItem + { + public string Name { get; set; } = ""; + + public string FilePath { get; set; } = ""; + } +} diff --git a/Model/Models/TcpConfig.cs b/Model/Models/TcpConfig.cs new file mode 100644 index 0000000..6982461 --- /dev/null +++ b/Model/Models/TcpConfig.cs @@ -0,0 +1,17 @@ +namespace Model.Models +{ + /// + /// TCP 通信参数(DeviceCommand 内部纯数据类,供设备类构造函数使用)。 + /// 与 UIShare.UIViewModel.TcpConfigVM 字段一一对应,由 DeviceManager 在实例化时填充。 + /// + public class TcpConfig + { + 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; + } +} diff --git a/Model/Models/TestReportModel.cs b/Model/Models/TestReportModel.cs new file mode 100644 index 0000000..c332c10 --- /dev/null +++ b/Model/Models/TestReportModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Models +{ + public class TestReportModel + { + public Guid TestRoundId { get; set; } + public string Scope { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string FileName { get; set; } + } +} diff --git a/Model/Result.cs b/Model/Result.cs new file mode 100644 index 0000000..6582ea0 --- /dev/null +++ b/Model/Result.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model +{ + public class Result + { + public Result() + { + + } + private int _code; + private readonly string _msg; + + /// + /// 结果是否成功 + /// + public bool IsSuccess => _code == 0; + + /// + /// 结果代码 + /// + public int Code { set => _code = value; get => _code; } + + /// + /// 结果消息 + /// + public string Msg => _msg; + + /// + /// 构造方法 + /// + /// + /// + /// + protected Result(int code, string msg, Exception? exception = null) + { + if (exception != null) + { + var listMoreMsg = new List(); + if (exception is ResultException resultException) + { + listMoreMsg.AddRange(resultException.MessageList); + } + //else if (exception is DeviceControlException deviceControlException) + //{ + // listMoreMsg.Add(deviceControlException.ErrorInfo); + //} + else + { + listMoreMsg.Add(exception.Message); + } + var strMoreMsg = string.Join("、", listMoreMsg.Where(it => !string.IsNullOrWhiteSpace(it))); + if (!string.IsNullOrWhiteSpace(strMoreMsg)) + { + msg += $"({strMoreMsg})"; + } + } + _code = code; + _msg = msg; + } + + /// + /// 返回成功结果 + /// + /// + /// + public static Result Success() + { + return new Result(0, ""); + } + + /// + /// 返回错误结果 + /// + /// + /// + /// + public static Result Error(string msg, Exception? exception = null) + { + return new Result(-1, msg, exception); + } + + + } + + public class Result : Result + { + public Result() + { + + } + private T? _data; + + /// + /// 结果数据 + /// + public T? Data { set => _data = value; get => _data; } + + /// + /// 构造方法 + /// + /// + /// + /// + /// + private Result(int code, string msg, T? data, Exception? exception = null) : base(code, msg, exception) + { + _data = data; + } + + /// + /// 返回成功数据 + /// + /// + /// + public static Result Success(T data) + { + return new Result(0, "", data); + } + + /// + /// 返回失败信息 + /// + /// + /// + /// + public new static Result Error(string msg, Exception? exception = null) + { + return new Result(-1, msg, default, exception); + } + + /// + /// 返回失败信息和数据 + /// + /// + /// + /// + /// + public static Result Error(string msg, T data, Exception? exception = null) + { + return new Result(-1, msg, data, exception); + } + + + } + + public class ResultException : Exception + { + private List? _messageList; + + public List MessageList + { + get + { + List messages = new(); + if (_messageList != null) + { + messages.AddRange(_messageList.Where(it => !string.IsNullOrWhiteSpace(it))); + } + return messages; + } + } + + public override string Message => string.Join(", ", MessageList); + + public ResultException() : base(null) + { + } + + public void AddAdditionMessage(string message) + { + _messageList ??= new(); + _messageList.Add(message); + } + } +} diff --git a/MonitorModule/MonitorModule.cs b/MonitorModule/MonitorModule.cs new file mode 100644 index 0000000..fb6d2f5 --- /dev/null +++ b/MonitorModule/MonitorModule.cs @@ -0,0 +1,24 @@ + +using MonitorModule.Views; +using MonitorModule.Views.Dialogs; +using System.Reflection; + +namespace MonitorModule +{ + [Module(OnDemand = true)] + public class MonitorModule: IModule + { + public void OnInitialized(IContainerProvider containerProvider) + { + IRegionManager regionManager = containerProvider.Resolve(); + } + + public void RegisterTypes(IContainerRegistry containerRegistry) + { + containerRegistry.RegisterForNavigation("MonitorView"); + containerRegistry.RegisterForNavigation("RecordView"); + containerRegistry.RegisterDialog("ValueLimitView"); + } + } + +} diff --git a/MonitorModule/MonitorModule.csproj b/MonitorModule/MonitorModule.csproj new file mode 100644 index 0000000..45b0b00 --- /dev/null +++ b/MonitorModule/MonitorModule.csproj @@ -0,0 +1,18 @@ + + + + net8.0-windows + enable + true + enable + + + + + + + + + + + diff --git a/MonitorModule/ViewModels/Dialogs/ValueLimitViewModel.cs b/MonitorModule/ViewModels/Dialogs/ValueLimitViewModel.cs new file mode 100644 index 0000000..6c6b01a --- /dev/null +++ b/MonitorModule/ViewModels/Dialogs/ValueLimitViewModel.cs @@ -0,0 +1,398 @@ +using Common.Attributes; +using DeviceCommand.Base; +using Prism.Events; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Windows.Input; +using UIShare.GlobalVariable; +using UIShare.PubEvent; +using UIShare.UIViewModel; +using UIShare.ViewModelBase; +using ZLGUSBCANFD; + +namespace MonitorModule.ViewModels.Dialogs +{ + public class ValueLimitViewModel : DialogViewModelBase + { + #region 属性 + private string _Title = "报警设置"; + + public string Title + { + get { return _Title; } + set { SetProperty(ref _Title, value); } + } + + private ValueLimitVM _SelectValueLimitVM; + + public ValueLimitVM SelectValueLimitVM + { + get => _SelectValueLimitVM; + set => SetProperty(ref _SelectValueLimitVM, value); + } + private ObservableCollection _ValueLimitList; + public ObservableCollection ValueLimitList + { + get => _ValueLimitList; + set => SetProperty(ref _ValueLimitList, value); + } + //private ObservableCollection _DeviceSingleList = new(); + + //public ObservableCollection DeviceSingleList + //{ + // get => _DeviceSingleList; + // set => SetProperty(ref _DeviceSingleList, value); + //} + //private string _SelectCurve; + + //public string SelectCurve + //{ + // get => _SelectCurve; + // set => SetProperty(ref _SelectCurve, value); + //} + + #endregion + #region 命令 + public ICommand SaveCommand { get; set; } + public ICommand CloseCommand { get; set; } + //public ICommand SelectSignalCommand { get; set; } + public ICommand DeleteCommand { get; set; } + + #endregion + private GlobalInfo _globalInfo { get; set; } + private SystemConfig _systemConfig { get; set; } + private IScopedProvider _scope { get; set; } + private DeviceManager? _deviceManager; + + /// SignalName → CAN信号标识,用于DBC加载/卸载时清理 + private readonly Dictionary _canSignalMap = new(); + + private SubscriptionToken? _dbcLoadedToken; + private SubscriptionToken? _dbcUnloadedToken; + + public ValueLimitViewModel(IContainerProvider containerProvider) : base(containerProvider) + { + _globalInfo = containerProvider.Resolve(); + _eventAggregator = containerProvider.Resolve(); + SaveCommand = new DelegateCommand(OnSave); + CloseCommand = new DelegateCommand(OnClose); + //SelectSignalCommand = new DelegateCommand(SelectSignal); + DeleteCommand = new DelegateCommand(Deletel); + } + #region 命令处理 + private void Deletel() + { + if (SelectValueLimitVM == null) + { + return; + } + else + { + ValueLimitList.Remove(SelectValueLimitVM); + SelectValueLimitVM = null; + } + } + + + private void InitData() + { + ValueLimitList = new ObservableCollection(); + //DeviceSingleList = new ObservableCollection(); + _canSignalMap.Clear(); + + // 1. 加载已保存的 ValueLimitList(保留用户设置) + if (_systemConfig?.ValueLimitList != null) + { + foreach (var item in _systemConfig.ValueLimitList) + ValueLimitList.Add(item); + } + + // 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList + foreach (var (displayName, fingerprint, methodName) in DiscoverDeviceSignals()) + { + //DeviceSingleList.Add(displayName); + EnsureValueLimit(fingerprint, displayName, displayName, methodName); + } + + // 3. 发现 CAN 信号并加入 ValueLimitList,同时收集有效信号名 + var validCanSignalNames = new HashSet(); + foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals()) + { + _canSignalMap[displayName] = (fingerprint, methodName); + EnsureValueLimit(fingerprint, displayName, displayName, methodName); + validCanSignalNames.Add(displayName); + } + + // 4. 清理已不存在于 DBC 中的 CAN 信号(Fingerprint 以 "CAN" 开头且不在有效集合中) + var staleCanSignals = ValueLimitList + .Where(x => !string.IsNullOrEmpty(x.Fingerprint) && + x.Fingerprint.StartsWith("CAN") && + !validCanSignalNames.Contains(x.SignalName)) + .ToList(); + foreach (var item in staleCanSignals) + ValueLimitList.Remove(item); + } + + /// + /// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。 + /// 返回 (DisplayName, Fingerprint, MethodName) 列表。 + /// + private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverDeviceSignals() + { + if (_deviceManager?.DeviceMap == null) yield break; + + var instanceToFp = BuildInstanceToFingerprint(); + foreach (var kvp in _deviceManager.DeviceMap) + { + string deviceName = kvp.Key; + var device = kvp.Value; + + if (!instanceToFp.TryGetValue(device, out string? fingerprint)) + continue; + + var methods = device.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => + { + if (m.GetCustomAttribute() == null) return false; + if (m.ReturnType != typeof(Task)) return false; + var parms = m.GetParameters(); + return parms.Length == 0 || + (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); + }); + + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + string displayName = !string.IsNullOrEmpty(attr?.Description) + ? $"{deviceName}.{attr.Description}" + : $"{deviceName}.{method.Name}"; + yield return (displayName, fingerprint, method.Name); + } + } + } + + /// + /// 构建设备实例 → 硬件指纹的反向查找表。 + /// + private Dictionary BuildInstanceToFingerprint() + { + var map = new Dictionary(ReferenceEqualityComparer.Instance); + if (_globalInfo?.HardwarePool == null) return map; + + foreach (var poolEntry in _globalInfo.HardwarePool) + { + var lazy = poolEntry.Value; + if (lazy.IsValueCreated && lazy.Value != null) + map[lazy.Value] = poolEntry.Key; + } + return map; + } + + /// + /// 发现当前已加载 DBC 中的 CAN 信号(以 ConfigurationList 为白名单)。 + /// 返回 (DisplayName, Fingerprint, MethodName) 列表。 + /// + private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverCanSignals() + { + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) yield break; + if (_systemConfig?.ConfigurationList == null) yield break; + + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint(); + foreach (var cfg in _systemConfig.ConfigurationList) + { + if (string.IsNullOrEmpty(cfg.SignalName)) continue; + if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue; + + string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, (uint)cfg.Channel); + string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName); + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + + if (ExistsInDbc(methodName, msgDb[cfg.Channel])) + yield return (displayName, fingerprint, methodName); + } + } + + /// + /// 判断指定的 MethodName 是否存在于该通道的 DBC 消息数据库中。 + /// + private static bool ExistsInDbc(string methodName, List<_Msg_> messages) + { + if (string.IsNullOrEmpty(methodName)) return false; + int dotIndex = methodName.IndexOf('.'); + if (dotIndex <= 0 || dotIndex >= methodName.Length - 1) return false; + + if (!uint.TryParse(methodName.Substring(0, dotIndex), System.Globalization.NumberStyles.HexNumber, null, out uint msgId)) + return false; + string signalName = methodName.Substring(dotIndex + 1); + + return messages.Any(m => m.msg_id == msgId && m.signal_Name != null && m.signal_Name.Contains(signalName)); + } + + /// + /// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。 + /// + private void EnsureValueLimit(string fingerprint, string displayName, string signalName, string methodName = "") + { + if (ValueLimitList.Any(x => x.SignalName == signalName)) return; + + ValueLimitList.Add(new ValueLimitVM + { + DisplayName = displayName, + Fingerprint = fingerprint, + SignalName = signalName, + MethodName = methodName, + Upper = 9999, + Lower = -9999, + UpperExtreme = 9999, + LowerExtreme = -9999, + }); + } + + /// + /// 从 ValueLimitList 和 DeviceSingleList 中移除指定信号。 + /// + private void RemoveValueLimit(string signalName) + { + var item = ValueLimitList.FirstOrDefault(x => x.SignalName == signalName); + if (item != null) ValueLimitList.Remove(item); + + //if (DeviceSingleList.Contains(signalName)) + // DeviceSingleList.Remove(signalName); + } + + //private void SelectSignal() + //{ + // if (ValueLimitList.Where(x => x.SignalName == SelectCurve).Count() > 0) + // { + // return; + // } + // ValueLimitList.Add(new ValueLimitVM + // { + // SignalName = SelectCurve, + // Upper = 9999, + // Lower = -9999, + // UpperExtreme = 9999, + // LowerExtreme = -9999, + // }); + //} + + private void OnClose() + { + RequestClose.Invoke(); + } + + private void OnSave() + { + _systemConfig.ValueLimitList = ValueLimitList; + ConfigService.Save(_systemConfig); + } + #endregion + public override void OnDialogOpened(IDialogParameters parameters) + { + // 1. 优先执行基类的打开逻辑(如果基类有需要初始化的通用事务) + base.OnDialogOpened(parameters); + + // 2. 解析参数 + if (parameters != null && parameters.ContainsKey("Scope")) // 修复:Prism 中应使用 ContainsKey + { + _scope = parameters.GetValue("Scope"); + _systemConfig = _scope.Resolve(); + _deviceManager = _scope.Resolve(); + + // 3. 订阅 DBC 加载/卸载事件 + _dbcLoadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcLoaded, ThreadOption.UIThread); + _dbcUnloadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcUnloaded, ThreadOption.UIThread); + + // 4. 初始化数据 + InitData(); + } + } + + public override void OnDialogClosed() + { + base.OnDialogClosed(); + + // 取消 DBC 事件订阅,避免内存泄漏 + if (_dbcLoadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcLoadedToken); + if (_dbcUnloadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcUnloadedToken); + } + + /// + /// DBCLoadedEvent 回调:清理该通道中不存在于 DBC 的 CAN 信号限制项。 + /// + private void OnDbcLoaded(DBCLoadedArgs args) + { + if (args.Scope != _systemConfig?.Title) return; + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return; + + int channel = (int)args.Channel; + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + if (channel < 0 || channel >= msgDb.Count) return; + + string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint(); + string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel); + + // 1. 移除该通道中已失效的 CAN 信号限制项 + var staleSignals = _canSignalMap + .Where(kvp => kvp.Value.Fingerprint == fingerprint && + !ExistsInDbc(kvp.Value.MethodName, msgDb[channel])) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var signalName in staleSignals) + { + RemoveValueLimit(signalName); + _canSignalMap.Remove(signalName); + } + + // 2. 根据 ConfigurationList 补充新加载的有效信号 + if (_systemConfig?.ConfigurationList != null) + { + foreach (var cfg in _systemConfig.ConfigurationList.Where(c => c.Channel == channel)) + { + if (string.IsNullOrEmpty(cfg.SignalName)) continue; + + string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName); + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + + if (ExistsInDbc(methodName, msgDb[channel])) + { + _canSignalMap[displayName] = (fingerprint, methodName); + //if (!DeviceSingleList.Contains(displayName)) + // DeviceSingleList.Add(displayName); + EnsureValueLimit(fingerprint, displayName, displayName, methodName); + } + } + } + } + + /// + /// DBCUnloadedEvent 回调:删除该通道所有 CAN 信号的限制项。 + /// + private void OnDbcUnloaded(DBCUnloadedArgs args) + { + if (args.Scope != _systemConfig?.Title) return; + + string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint(); + string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel); + var toRemove = _canSignalMap + .Where(kvp => kvp.Value.Fingerprint == fingerprint) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var signalName in toRemove) + { + RemoveValueLimit(signalName); + _canSignalMap.Remove(signalName); + } + } + } +} diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs new file mode 100644 index 0000000..4843448 --- /dev/null +++ b/MonitorModule/ViewModels/MonitorViewModel.cs @@ -0,0 +1,852 @@ +using Common.Attributes; +using Model.Entity; +using Model.Models; +using NLog; +using OxyPlot; +using OxyPlot.Axes; +using OxyPlot.Legends; +using OxyPlot.Series; +using Service.Interface; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using System.Windows.Threading; +using UIShare.GlobalVariable; +using UIShare.PubEvent; +using UIShare.UIViewModel; +using UIShare.ViewModelBase; +using ZLGUSBCANFD; + + +namespace MonitorModule.ViewModels +{ + public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable + { + #region 属性 + private IMonitorValueService _monitorValueService; + public bool KeepAlive => true; + public ScopedContext _scopedContext { get; set; } = null!; + public GlobalInfo _globalInfo { get; set; } + + private string _testStatus = string.Empty; + public string TestStatus + { + get => _testStatus; + set => SetProperty(ref _testStatus, value); + } + + public PlotModel Plot { get; } + + /// 已添加的监测通道列表 + public ObservableCollection Channels + { + get => _systemConfig?.Channels ?? new ObservableCollection(); + set + { + if (_systemConfig != null && _systemConfig.Channels != value) + { + _systemConfig.Channels = value; + RaisePropertyChanged(); + } + } + } + + + /// 可添加的设备方法列表(供用户选择) + public ObservableCollection AvailableMethods { get; } = new(); + + private MonitorChannelVM? _selectedChannel; + public MonitorChannelVM? SelectedChannel + { + get => _selectedChannel; + set => SetProperty(ref _selectedChannel, value); + } + + private AvailableMethodItem? _selectedAvailableMethod; + public AvailableMethodItem? SelectedAvailableMethod + { + get => _selectedAvailableMethod; + set => SetProperty(ref _selectedAvailableMethod, value); + } + + private string _statusMessage = "图表已就绪,暂无监测项"; + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + #endregion + + #region 命令 + public ICommand AddChannelCommand { get; } + public ICommand DeleteChannelCommand { get; } + public ICommand ResetViewCommand { get; } + public ICommand RefreshDataCommand { get; } + public ICommand RefreshCommand { get; } + public ICommand SaveCommand { get; } + #endregion + + #region 私有字段 + private bool IsInitiated = false; + private IScopedProvider? _scope; + private DeviceManager? _deviceManager; + private SystemConfig? _systemConfig; + private CancellationTokenSource? _monitorCTS = new(); + + // 颜色调色盘 + 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; + + /// 设备数据广播器(全局单例) + private HardwareDataBroadcaster? _broadcaster; + + /// CAN 信号广播器(全局单例) + private CANSignalBroadcaster? _canSignalBroadcaster; + + /// 用于 OxyPlot X 轴相对秒数 + private readonly Stopwatch _stopwatch = new(); + + /// OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot) + private readonly DispatcherTimer _plotRefreshTimer; + + /// 事件订阅令牌,用于 Dispose 时取消订阅 + private SubscriptionToken? _subscriptionToken; + private SubscriptionToken? _dbcLoadedToken; + private SubscriptionToken? _dbcUnloadedToken; + + // ========================================== + // 批量入库核心结构 + // ========================================== + private readonly ConcurrentQueue _insertQueue = new(); + private Task? _dbFlushTask; + private const int BulkInsertThreshold = 50; + private int _isFlushing = 0; + #endregion + + public MonitorViewModel(IContainerExtension container) : base(container) + { + _globalInfo = container.Resolve(); + _monitorValueService = container.Resolve(); + + Plot = BuildEmptyPlot(); + + AddChannelCommand = new DelegateCommand(OnAddChannel); + DeleteChannelCommand = new DelegateCommand(OnDeleteChannel); + ResetViewCommand = new DelegateCommand(OnResetView); + RefreshDataCommand = new DelegateCommand(OnRefreshData); + RefreshCommand = new DelegateCommand(OnExpand); + SaveCommand = new DelegateCommand(OnSave); + + // OxyPlot 刷新定时器(1000ms,只负责视觉更新) + _plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background) + { + Interval = TimeSpan.FromMilliseconds(1000) + }; + _plotRefreshTimer.Tick += OnPlotRefreshTick; + } + + + + public void Dispose() + { + _monitorCTS?.Cancel(); + _monitorCTS?.Dispose(); + _monitorCTS = null; + + _plotRefreshTimer.Stop(); + _plotRefreshTimer.Tick -= OnPlotRefreshTick; + _stopwatch.Stop(); + + // 取消 EventAggregator 订阅 + if (_subscriptionToken != null) + _eventAggregator.GetEvent().Unsubscribe(_subscriptionToken); + if (_dbcLoadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcLoadedToken); + if (_dbcUnloadedToken != null) + _eventAggregator.GetEvent().Unsubscribe(_dbcUnloadedToken); + + if (_dbFlushTask != null) + { + try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { } + } + + + // 3. 释放容器作用域 + _scope?.Dispose(); + Channels.Clear(); + AvailableMethods.Clear(); + } + private void OnSave() + { + // 将当前 Channels 序列化为 MonitorChannelConfig 列表保存到 MonitorChannels + if (_systemConfig != null) + { + _systemConfig.MonitorChannels = new ObservableCollection(Channels + .Select(c => new MonitorChannelConfig + { + Fingerprint = c.Fingerprint, + MethodName = c.MethodName, + IsDisplayed = c.IsDisplayed + })); + } + ConfigService.Save(_systemConfig); + } + #region 事件驱动:接收广播数据 + /// + /// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。 + /// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。 + /// 通过 HardwareFingerprint + MethodName 匹配监测通道。 + /// + private void OnHardwareDataReceived(HardwareReportArgs args) + { + // 仅处理属于当前 Scope 的事件 + if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return; + + // 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦) + var channel = Channels.FirstOrDefault(c => + c.Fingerprint == args.HardwareFingerprint && + c.MethodName == args.MethodName && + c.IsMonitored); + + if (channel == null) return; + + // 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作) + channel.Record(args.Time, args.Value); + + // 入队数据库批量写入 + // CreateTime 使用 args.Time(采样触发时刻),而不是 DateTime.Now(设备响应到达时刻), + // 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。 + var entity = new MonitorValueEntity + { + MonitorName = channel.DisplayName, + MonitorValue = args.Value, + Scope = args.Scope, + CreateTime = args.Time, + IsDel = 0 + }; + _insertQueue.Enqueue(entity); + + if (_insertQueue.Count >= BulkInsertThreshold) + { + _ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false)); + } + } + #endregion + + #region OxyPlot 视觉刷新定时器 + private void OnPlotRefreshTick(object? sender, EventArgs e) + { + if (!_stopwatch.IsRunning) return; + + var now = DateTime.Now; + var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom); + if (xAxis != null) + { + xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20)); + xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5)); + } + Plot.InvalidatePlot(true); + } + #endregion + + #region 后台数据库批量入库 + private void StartDbFlushWorker(CancellationToken token) + { + _dbFlushTask = Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false); + await DoFlushWorkAsync().ConfigureAwait(false); + } + catch (OperationCanceledException) { break; } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"[DB Worker Error] {ex.Message}"); + } + } + await DoFlushWorkAsync().ConfigureAwait(false); + }, token); + } + + private async Task DoFlushWorkAsync() + { + if (_insertQueue.IsEmpty) return; + if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return; + + try + { + var listToInsert = new List(); + while (_insertQueue.TryDequeue(out var entity)) listToInsert.Add(entity); + + if (listToInsert.Count > 0) + { + var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false); + } + } + finally { Interlocked.Exchange(ref _isFlushing, 0); } + } + #endregion + + #region Navigation 导航 + public override void OnNavigatedTo(NavigationContext navigationContext) + { + base.OnNavigatedTo(navigationContext); + if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name")) + { + TestStatus = navigationContext.Parameters.GetValue("Name"); + Plot.Title = $"监控 - {TestStatus}"; + Plot.InvalidatePlot(false); + + _scope = _globalInfo.ScopeDic[TestStatus]; + _scopedContext = _scope.Resolve(); + _deviceManager = _scope.Resolve(); + _systemConfig = _scope.Resolve(); + RaisePropertyChanged(nameof(Channels)); + _broadcaster = _scope.Resolve(); + _canSignalBroadcaster = _scope.Resolve(); + IsInitiated = true; + + if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource(); + + // 1. 启动后台数据库消费 + StartDbFlushWorker(_monitorCTS.Token); + + // 2. 扫描可监测方法(供 UI 选择) + DiscoverAvailableMethods(); + + // 3. 从配置自动恢复已保存的监测通道 + RestoreChannelsFromConfig(); + + // 4. 启动广播器(Discover + Start) + _broadcaster.Discover(); + _broadcaster.Start(); + _canSignalBroadcaster.Discover(); + _canSignalBroadcaster.Start(); + + // 5. 订阅 HardwareDataReportedEvent(UI 线程回调) + _subscriptionToken = _eventAggregator.GetEvent() + .Subscribe(OnHardwareDataReceived, ThreadOption.UIThread); + + // 5b. 订阅 DBC 加载/卸载事件,动态发现或清除 CAN 信号 + _dbcLoadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcLoaded, ThreadOption.UIThread); + _dbcUnloadedToken = _eventAggregator.GetEvent() + .Subscribe(OnDbcUnloaded, ThreadOption.UIThread); + + // 5c. 根据 SystemConfig.ConfigurationList 刷新已加载 DBC 中的 CAN 信号 + RefreshConfiguredCanSignals(); + + // 6. 启动 OxyPlot 视觉刷新定时器 + _stopwatch.Start(); + _plotRefreshTimer.Start(); + } + } + #endregion + + #region PlotModel 构建 + private static PlotModel BuildEmptyPlot() + { + var pm = new PlotModel + { + Title = string.Empty, + 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 设备方法发现(仅用于 UI 展示可选项) + /// + /// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。 + /// + private Dictionary BuildInstanceToFingerprint() + { + var map = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var poolEntry in _globalInfo.HardwarePool) + { + var lazy = poolEntry.Value; + if (lazy.IsValueCreated && lazy.Value != null) + map[lazy.Value] = poolEntry.Key; + } + return map; + } + + private void DiscoverAvailableMethods() + { + AvailableMethods.Clear(); + if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0) + { + StatusMessage = "当前工位无可用设备"; + return; + } + + var instanceToFp = BuildInstanceToFingerprint(); + + foreach (var kvp in _deviceManager.DeviceMap) + { + string deviceName = kvp.Key; + var device = kvp.Value; + var deviceType = device.GetType(); + + // 反查该实例的硬件指纹 + if (!instanceToFp.TryGetValue(device, out string? fingerprint)) + continue; + + var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => + { + if (m.GetCustomAttribute() == null) return false; + if (m.ReturnType != typeof(Task)) return false; + var parms = m.GetParameters(); + return parms.Length == 0 || + (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); + }); + + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + string displayName = !string.IsNullOrEmpty(attr?.Description) + ? $"{deviceName}.{attr.Description}" + : $"{deviceName}.{method.Name}"; + + AvailableMethods.Add(new AvailableMethodItem + { + DeviceName = deviceName, + Fingerprint = fingerprint, + MethodName = method.Name, + DisplayName = displayName, + MethodInfo = method, + Device = device + }); + } + } + + StatusMessage = AvailableMethods.Count > 0 + ? $"发现 {AvailableMethods.Count} 个可监测项" + : "未发现可监测的设备方法"; + } + + /// + /// 从 SystemConfig.MonitorChannels 持久化列表自动还原监测通道。 + /// + private void RestoreChannelsFromConfig() + { + if (_systemConfig?.MonitorChannels == null || _systemConfig.MonitorChannels.Count == 0) return; + + int restored = 0; + foreach (var config in _systemConfig.MonitorChannels) + { + if (string.IsNullOrWhiteSpace(config.Fingerprint) || + string.IsNullOrWhiteSpace(config.MethodName)) continue; + + // 已在 Channels 中存在则跳过 + if (Channels.Any(c => c.Fingerprint == config.Fingerprint && c.MethodName == config.MethodName)) + continue; + + // 在 AvailableMethods 中查找匹配项 + var method = AvailableMethods.FirstOrDefault(m => + m.Fingerprint == config.Fingerprint && m.MethodName == config.MethodName); + if (method == null) continue; + + var color = _palette[_colorIndex % _palette.Length]; + _colorIndex++; + + var channel = new MonitorChannelVM + { + DeviceName = method.DeviceName, + Fingerprint = method.Fingerprint, + MethodName = method.MethodName, + DisplayName = method.DisplayName, + Color = color, + IsMonitored = true, + IsDisplayed = config.IsDisplayed + }; + + // 仅当 IsDisplayed=true 时才创建 LineSeries + if (channel.IsDisplayed) + { + channel.Series = new LineSeries + { + Title = channel.DisplayName, + Color = color, + StrokeThickness = 1.5 + }; + Plot.Series.Add(channel.Series); + } + + channel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed)) + OnChannelDisplayChanged(channel); + }; + + Channels.Add(channel); + restored++; + } + + if (restored > 0) + { + Plot.InvalidatePlot(true); + StatusMessage = $"已从配置自动恢复 {restored} 个监测通道"; + } + } + + /// + /// 根据 SystemConfig.ConfigurationList 刷新所有已加载 DBC 中的 CAN 信号。 + /// 只添加 ConfigurationList 中配置且实际存在于 DBC 的信号;不存在则清理。 + /// + private void RefreshConfiguredCanSignals() + { + if (_systemConfig?.ConfigurationList == null || _systemConfig.ConfigurationList.Count == 0) return; + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return; + + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + foreach (var channel in _systemConfig.ConfigurationList.Select(c => c.Channel).Distinct()) + { + if (channel < 0 || channel >= msgDb.Count) continue; + RefreshCanSignalsForChannel((uint)channel, msgDb[channel]); + } + } + + /// + /// 刷新单个通道的 CAN 信号:以 ConfigurationList 为白名单,对照 DBC 实际存在性。 + /// - DBC 中存在 → 加入 AvailableMethods;若 MonitorChannels 中也有,则恢复 Channel。 + /// - DBC 中不存在 → 从 AvailableMethods 移除;若已在 Channels 中,则删除。 + /// + private void RefreshCanSignalsForChannel(uint channel, List<_Msg_> messages) + { + if (_systemConfig?.ConfigurationList == null) return; + + string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty; + string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, channel); + var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList(); + bool changed = false; + + foreach (var cfg in configs) + { + if (string.IsNullOrEmpty(cfg.SignalName)) continue; + + string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName); + bool existsInDbc = messages.Any(m => + m.msg_id == cfg.MessageID && + m.signal_Name != null && + m.signal_Name.Contains(cfg.SignalName)); + + if (existsInDbc) + { + // 加入 AvailableMethods(去重) + if (!AvailableMethods.Any(m => m.Fingerprint == fingerprint && m.MethodName == methodName)) + { + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + AvailableMethods.Add(new AvailableMethodItem + { + DeviceName = $"CAN{channel}", + Fingerprint = fingerprint, + MethodName = methodName, + DisplayName = displayName, + MethodInfo = null!, + Device = null! + }); + changed = true; + } + + // 如果 MonitorChannels 中有持久化记录,自动恢复 Channel + var monitorConfig = _systemConfig.MonitorChannels?.FirstOrDefault(m => + m.Fingerprint == fingerprint && m.MethodName == methodName); + if (monitorConfig != null && !Channels.Any(c => + c.Fingerprint == fingerprint && c.MethodName == methodName)) + { + AddCanChannel(cfg, fingerprint, methodName, monitorConfig.IsDisplayed); + changed = true; + } + } + else + { + // DBC 中不存在:从 AvailableMethods 移除 + var methodToRemove = AvailableMethods.FirstOrDefault(m => + m.Fingerprint == fingerprint && m.MethodName == methodName); + if (methodToRemove != null) + { + AvailableMethods.Remove(methodToRemove); + changed = true; + } + + // 如果该信号已经在 Channels(或 MonitorChannels)中,删除 + var channelToRemove = Channels.FirstOrDefault(c => + c.Fingerprint == fingerprint && c.MethodName == methodName); + if (channelToRemove != null) + { + if (channelToRemove.Series != null) Plot.Series.Remove(channelToRemove.Series); + Channels.Remove(channelToRemove); + changed = true; + } + } + } + + if (changed) + { + Plot.InvalidatePlot(true); + StatusMessage = $"CAN{channel} 已刷新配置信号"; + } + } + + /// + /// DBCLoadedEvent 回调:DBC 加载后刷新该通道的配置信号。 + /// + private void OnDbcLoaded(DBCLoadedArgs args) + { + if (args.Scope != TestStatus) return; + if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return; + + int channel = (int)args.Channel; + var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase; + if (channel < 0 || channel >= msgDb.Count) return; + + RefreshCanSignalsForChannel(args.Channel, msgDb[channel]); + } + + /// + /// DBCUnloadedEvent 回调:移除对应通道的 CAN 信号(AvailableMethods + Channels)。 + /// + private void OnDbcUnloaded(DBCUnloadedArgs args) + { + if (args.Scope != TestStatus) return; + + string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty; + string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel); + + // 从 AvailableMethods 中移除 + var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList(); + foreach (var m in toRemoveMethods) + AvailableMethods.Remove(m); + + // 从 Channels 中移除(并清理 Plot) + var toRemoveChannels = Channels.Where(c => c.Fingerprint == fingerprint).ToList(); + foreach (var c in toRemoveChannels) + { + if (c.Series != null) Plot.Series.Remove(c.Series); + Channels.Remove(c); + } + + if (toRemoveMethods.Count > 0 || toRemoveChannels.Count > 0) + { + Plot.InvalidatePlot(true); + StatusMessage = $"CAN{args.Channel} DBC 已卸载,移除 {toRemoveMethods.Count} 个信号"; + } + } + + /// + /// 将单个 CAN 信号添加为监测通道。 + /// + private void AddCanChannel(CANSignalConfig cfg, string fingerprint, string methodName, bool isDisplayed) + { + var color = _palette[_colorIndex % _palette.Length]; + _colorIndex++; + + string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName); + var channel = new MonitorChannelVM + { + DeviceName = $"CAN{cfg.Channel}", + Fingerprint = fingerprint, + MethodName = methodName, + DisplayName = displayName, + Color = color, + IsMonitored = true, + IsDisplayed = isDisplayed + }; + + if (channel.IsDisplayed) + { + channel.Series = new LineSeries + { + Title = channel.DisplayName, + Color = color, + StrokeThickness = 1.5 + }; + Plot.Series.Add(channel.Series); + } + + channel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed)) + OnChannelDisplayChanged(channel); + }; + + Channels.Add(channel); + } + #endregion + + #region 命令处理 + private void OnAddChannel() + { + var method = SelectedAvailableMethod; + if (method == null) + { + StatusMessage = "请先在列表中选择一个监测项"; + return; + } + + // 以指纹+方法名去重,防止同一物理设备重复添加 + if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName)) + { + StatusMessage = $"监测项 [{method.DisplayName}] 已存在"; + return; + } + + var color = _palette[_colorIndex % _palette.Length]; + _colorIndex++; + + var channel = new MonitorChannelVM + { + DeviceName = method.DeviceName, + Fingerprint = method.Fingerprint, + MethodName = method.MethodName, + DisplayName = method.DisplayName, + Color = color, + IsMonitored = true, + IsDisplayed = true + }; + + channel.Series = new LineSeries + { + Title = channel.DisplayName, + Color = color, + StrokeThickness = 1.5 + }; + Plot.Series.Add(channel.Series); + + channel.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed)) + OnChannelDisplayChanged(channel); + }; + + Channels.Add(channel); + SelectedChannel = channel; + + Plot.InvalidatePlot(true); + StatusMessage = $"已添加监测项 [{channel.DisplayName}],当前共 {Channels.Count} 项"; + } + + private void OnDeleteChannel() + { + var target = SelectedChannel ?? Channels.LastOrDefault(); + if (target == null) + { + StatusMessage = "无可删除的监测项"; + return; + } + + if (target.Series != null) Plot.Series.Remove(target.Series); + Channels.Remove(target); + SelectedChannel = Channels.LastOrDefault(); + + Plot.InvalidatePlot(true); + StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项"; + } + + private void OnResetView() + { + Plot.ResetAllAxes(); + Plot.InvalidatePlot(false); + StatusMessage = "视图已按数据范围复原"; + } + + private void OnRefreshData() + { + Plot.InvalidatePlot(true); + StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss})"; + } + + private void OnExpand() + { + if (string.IsNullOrEmpty(TestStatus)) return; + _globalInfo.CurrentScope = TestStatus; + _eventAggregator.GetEvent().Publish(TestStatus); + } + #endregion + + #region 显示/隐藏切换与数学变换 + public void OnChannelDisplayChanged(MonitorChannelVM channel) + { + if (channel.IsDisplayed) + { + if (channel.Series == null) + { + channel.Series = new LineSeries + { + Title = channel.DisplayName, + Color = channel.Color, + StrokeThickness = 1.5 + }; + + var recent = channel.DataPoints.ToArray(); + var startIdx = Math.Max(0, recent.Length - 10000); + for (int i = startIdx; i < recent.Length; i++) + channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue)); + + Plot.Series.Add(channel.Series); + Plot.InvalidatePlot(true); + } + } + else + { + if (channel.Series != null) + { + Plot.Series.Remove(channel.Series); + channel.Series = null; + Plot.InvalidatePlot(true); + } + } + } + + public void OnChannelMathChanged(MonitorChannelVM channel) + { + if (channel.Series == null) return; + var points = channel.DataPoints.ToArray(); + channel.Series.Points.Clear(); + var startIdx = Math.Max(0, points.Length - 10000); + for (int i = startIdx; i < points.Length; i++) + channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue)); + Plot.InvalidatePlot(true); + } + #endregion + } +} diff --git a/MonitorModule/ViewModels/RecordViewModel.cs b/MonitorModule/ViewModels/RecordViewModel.cs new file mode 100644 index 0000000..bfb575f --- /dev/null +++ b/MonitorModule/ViewModels/RecordViewModel.cs @@ -0,0 +1,410 @@ +using Common.Attributes; +using DeviceCommand.Base; +using Logger; +using Model.Entity; +using ORM; +using SqlSugar; +using System.Collections.ObjectModel; +using System.Data; +using System.IO; +using System.Reflection; +using System.Text; +using System.Windows.Input; +using UIShare.GlobalVariable; +using UIShare.PubEvent; +using UIShare.ViewModelBase; + +namespace MonitorModule.ViewModels +{ + /// + /// 记录界面:查询 MonitorValueEntity 表中的历史监测数据。 + /// 功能:监测项筛选 / 台架筛选 / 日期筛选 / 分页 / 导出 CSV。 + /// 使用 SqlSugarContext.DbContext 全局单例,与写入端共用同一数据库连接。 + /// + public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable + { + + #region 属性 + public bool KeepAlive => true; + public ScopedContext _scopedContext { get; set; } + public GlobalInfo _globalInfo { get; } + + public string TestStatus + { + get => _testStatus; + set => SetProperty(ref _testStatus, value); + } + + /// 监测项名称下拉列表 + public ObservableCollection MonitorNames + { + get => _monitorNames; + set => SetProperty(ref _monitorNames, value); + } + + /// 选中的监测项(null = 全部) + public string? SelectedMonitorName + { + get => _selectedMonitorName; + set => SetProperty(ref _selectedMonitorName, value); + } + + /// 台架(作用域)下拉列表 + public ObservableCollection ScopeNames + { + get => _scopeNames; + set => SetProperty(ref _scopeNames, value); + } + + /// 选中的台架(null = 全部) + public string? SelectedScope + { + get => _selectedScope; + set => SetProperty(ref _selectedScope, value); + } + + /// 查询日期(null = 全部日期) + public DateTime? SelectedStartDate + { + get => _selectedStartDate; + set => SetProperty(ref _selectedStartDate, value); + } + public DateTime? SelectedEndDate + { + get => _selectedEndDate; + set => SetProperty(ref _selectedEndDate, value); + } + + public DataTable ResultTable + { + get => _resultTable; + set => SetProperty(ref _resultTable, value); + } + + public string StatusMessage + { + get => _statusMessage; + set => SetProperty(ref _statusMessage, value); + } + + public int PageIndex + { + get => _pageIndex; + set => SetProperty(ref _pageIndex, value); + } + + public int PageSize + { + get => _pageSize; + set + { + if (SetProperty(ref _pageSize, value <= 0 ? 50 : value)) + { + RaisePropertyChanged(nameof(TotalPages)); + PageIndex = 1; + Query(); + } + } + } + + public long TotalCount + { + get => _totalCount; + set + { + SetProperty(ref _totalCount, value); + RaisePropertyChanged(nameof(TotalPages)); + } + } + + public int TotalPages + { + get + { + if (PageSize <= 0) return 1; + var pages = (int)((TotalCount + PageSize - 1) / PageSize); + return pages <= 0 ? 1 : pages; + } + } + #endregion + + #region 命令 + public ICommand LoadedCommand { get; } + public ICommand QueryCommand { get; } + public ICommand ClearFilterCommand { get; } + public ICommand FirstPageCommand { get; } + public ICommand PrevPageCommand { get; } + public ICommand NextPageCommand { get; } + public ICommand LastPageCommand { get; } + public ICommand ExportCsvCommand { get; } + public ICommand RefreshCommand { get; } + #endregion + + #region 私有字段 + private IScopedProvider _scope; + private string _testStatus = string.Empty; + private ObservableCollection _monitorNames = new(); + private string? _selectedMonitorName; + private ObservableCollection _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" }); + private string? _selectedScope = "全部"; + private DateTime? _selectedStartDate; + private DateTime? _selectedEndDate; + private DataTable _resultTable = new(); + private string _statusMessage = "未连接"; + private int _pageIndex = 1; + private int _pageSize = 50; + private long _totalCount; + private bool IsInitiated = false; + #endregion + + public RecordViewModel(IContainerExtension container) : base(container) + { + _globalInfo = container.Resolve(); + LoadedCommand = new DelegateCommand(LoadFilterOptions); + QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); }); + ClearFilterCommand = new DelegateCommand(ClearFilter); + FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } }); + PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } }); + NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } }); + LastPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex = TotalPages; Query(); } }); + ExportCsvCommand = new DelegateCommand(ExportCsv); + RefreshCommand = new DelegateCommand(OnExpand); + } + + public void Dispose() + { + _scope?.Dispose(); + } + + #region 筛选条件加载 + /// + /// 加载筛选条件:监测项通过反射发现(与 MonitorViewModel 一致),台架从数据库查询。 + /// + private void LoadFilterOptions() + { + // 监测项:反射发现当前作用域所有带 [Monitorable] 的设备方法 + DiscoverMonitorNames(); + + Query(); + } + + /// + /// 反射扫描当前作用域 DeviceManager 中所有带 [Monitorable] 特性的设备方法, + /// 生成与 MonitorViewModel 完全一致的 DisplayName 列表,填充到下拉框。 + /// 这样即使数据库还没数据,用户也能看到所有可筛选的监测项。 + /// + private void DiscoverMonitorNames() + { + MonitorNames = new ObservableCollection(); + + if (_scope == null) return; + + try + { + var deviceManager = _scope.Resolve(); + if (deviceManager?.DeviceMap == null || deviceManager.DeviceMap.Count == 0) + { + StatusMessage = "当前工位无可用设备"; + return; + } + + // 构建 设备实例 → 硬件指纹 的反向查找表 + var instanceToFp = new Dictionary(ReferenceEqualityComparer.Instance); + foreach (var poolEntry in _globalInfo.HardwarePool) + { + var lazy = poolEntry.Value; + if (lazy.IsValueCreated && lazy.Value != null) + instanceToFp[lazy.Value] = poolEntry.Key; + } + + var names = new List(); + + foreach (var kvp in deviceManager.DeviceMap) + { + string deviceName = kvp.Key; + var device = kvp.Value; + var deviceType = device.GetType(); + + // 只列出在硬件指纹池中注册的设备 + if (!instanceToFp.ContainsKey(device)) + continue; + + var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => + { + if (m.GetCustomAttribute() == null) return false; + if (m.ReturnType != typeof(Task)) return false; + var parms = m.GetParameters(); + return parms.Length == 0 || + (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); + }); + + foreach (var method in methods) + { + var attr = method.GetCustomAttribute(); + string displayName = !string.IsNullOrEmpty(attr?.Description) + ? $"{deviceName}.{attr.Description}" + : $"{deviceName}.{method.Name}"; + names.Add(displayName); + } + } + + names.Sort(); + MonitorNames = new ObservableCollection(names); + } + catch (Exception ex) + { + LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"反射发现监测项失败:{ex.Message}"); + } + } + + /// 清除所有筛选条件 + private void ClearFilter() + { + SelectedMonitorName = null; + SelectedScope = "全部"; + SelectedStartDate = null; + SelectedEndDate = null; + PageIndex = 1; + Query(); + } + #endregion + + #region 查询 + /// + /// 按当前筛选条件分页查询 MonitorValueEntity。 + /// + private void Query() + { + try + { + var db = SqlSugarContext.DbContext; + + // 预计算日期范围,避免在表达式树中访问可空属性 + DateTime? dateStart = SelectedStartDate?.Date; + DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1); + + var query = db.Queryable() + .WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName) + .WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope) + .WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value) + .OrderBy(x => x.CreateTime, OrderByType.Desc); + + TotalCount = query.Count(); + + // 修正越界 + if (PageIndex > TotalPages) PageIndex = TotalPages; + if (PageIndex < 1) PageIndex = 1; + + int skip = (PageIndex - 1) * PageSize; + ResultTable = query + .Skip(skip) + .Take(PageSize) + .ToDataTable(); + + StatusMessage = $"共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页"; + } + catch (Exception ex) + { + LoggerHelper.ErrorWithNotify(TestStatus, $"查询失败:{ex.Message}"); + StatusMessage = $"查询失败:{ex.Message}"; + ResultTable = new DataTable(); + TotalCount = 0; + } + } + #endregion + + #region 导出 CSV + /// + /// 按当前筛选条件导出全部结果到 CSV(不分页)。 + /// + private void ExportCsv() + { + try + { + var db = SqlSugarContext.DbContext; + + DateTime? dateStart = SelectedStartDate?.Date; + DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1); + + var query = db.Queryable() + .WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName) + .WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope) + .WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value) + .OrderBy(x => x.CreateTime, OrderByType.Desc); + + var dt = query.ToDataTable(); + if (dt.Rows.Count == 0) + { + StatusMessage = "无可导出数据"; + return; + } + + var dlg = new Microsoft.Win32.SaveFileDialog + { + Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*", + FileName = $"MonitorData_{DateTime.Now:yyyyMMdd_HHmmss}.csv", + Title = "导出监测数据为 CSV" + }; + if (dlg.ShowDialog() != true) return; + + WriteCsv(dlg.FileName, dt); + + StatusMessage = $"导出完成:{dlg.FileName}({dt.Rows.Count} 行)"; + LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出监测数据至 {dlg.FileName},共 {dt.Rows.Count} 行"); + } + catch (Exception ex) + { + LoggerHelper.ErrorWithNotify(TestStatus, $"导出失败:{ex.Message}"); + StatusMessage = $"导出失败:{ex.Message}"; + } + } + + private static void WriteCsv(string path, DataTable dt) + { + // 写 UTF-8 BOM 让 Excel 直接识别中文 + using var sw = new StreamWriter(path, false, new UTF8Encoding(true)); + // 表头 + sw.WriteLine(string.Join(",", dt.Columns.Cast().Select(c => Escape(c.ColumnName)))); + // 行 + foreach (DataRow row in dt.Rows) + { + sw.WriteLine(string.Join(",", row.ItemArray.Select(v => Escape(v?.ToString() ?? string.Empty)))); + } + } + + private static string Escape(string field) + { + if (field.Contains('"') || field.Contains(',') || field.Contains('\r') || field.Contains('\n')) + { + return "\"" + field.Replace("\"", "\"\"") + "\""; + } + return field; + } + #endregion + + #region 双击展开 + private void OnExpand() + { + if (string.IsNullOrEmpty(TestStatus)) return; + _globalInfo.CurrentScope = TestStatus; + _eventAggregator.GetEvent().Publish(TestStatus); + } + #endregion + + #region 重写 + public override void OnNavigatedTo(NavigationContext navigationContext) + { + base.OnNavigatedTo(navigationContext); + if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name")) + { + TestStatus = navigationContext.Parameters.GetValue("Name"); + _scope = _globalInfo.ScopeDic[TestStatus]; + _scopedContext = _scope.Resolve(); + IsInitiated = true; + } + LoadFilterOptions(); + } + #endregion + } +} diff --git a/MonitorModule/Views/Dialogs/ValueLimitView.xaml b/MonitorModule/Views/Dialogs/ValueLimitView.xaml new file mode 100644 index 0000000..3153065 --- /dev/null +++ b/MonitorModule/Views/Dialogs/ValueLimitView.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + +