From fe2aa4880d5c9fd88088ad0ed81ebaca05629f5f Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 02 十一月 2026 17:59:45 +0800
Subject: [PATCH] feat: 集成CAN驱动/模拟器 + 仪器仪表注册/硬件调试模块
---
docs/TapProject_代码结构_02_预览.png | 0
Plugin.StandardTest/ScreenTouchWin.xaml | 20
WPFProject/BuildCmd.bat | 11
Config/LogViewer/log_2026-10-23.db | 0
Plugin.DeviceDebug/Panels/CanDebugPanel.xaml | 220
Plugin.CurrentSensorControl/Entity/BaseConfig.cs | 2
Plugin.TestResult/TestResultView.xaml.cs | 6
Plugin.TsExecution/Plugin.TsExecution.csproj | 8
Plugin.StandardTest/ScreenTouch.xaml.cs | 179
Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs | 33
Plugin.DeviceDebug/Entity/DebugSession.cs | 75
Aging.Infrastructure/Contracts/IResourceManager.cs | 84
Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs | 248
Plugin.IntegrationTest/MainWindow.xaml.cs | 840 ++
Plugin.DeviceConfig/BatchGenerateModal.xaml.cs | 252
Plugin.DeviceConfig/DeviceEditModal.xaml | 182
Aging.Infrastructure/Services/DeviceService.cs | 331
WPFProject/PanuonWindow.xaml.cs | 11
Plugin.DeviceConfig/Entity/DeviceChannel.cs | 57
Util.ManualDebug/StatusBtn.xaml | 4
Util.ManualDebug/StatusCell.xaml.cs | 104
Util.ManualDebug/StatusCell.xaml | 39
docs/TapProject_代码结构分析.md | 55
Aging.Infrastructure/Core/OperationLease.cs | 82
Aging.Infrastructure/Drivers/DriverFactory.cs | 110
Plugin.DeviceDebug/Entity/DebugOperation.cs | 27
Plugin.DeviceDebug/DeviceDebug.xaml | 177
Util.ManualDebug/ManualDebug.xaml | 237
WPFProject/App.config | 3
WPFProject/Config/Database/database.db | 0
Plugin.TsExecution/TsExecution.xaml.cs | 100
WPFProject/TapProject.sln | 232
WPFProject/TapProject.csproj | 57
Aging.Infrastructure/Drivers/CanDriverAdapter.cs | 576 +
WPFProject/App.xaml | 5
WPFProject/packages.config | 1
Plugin.TestPlanEditor/EditorUI.xaml.cs | 29
Plugin.IntegrationTest/App.xaml.cs | 8
Plugin.StandardTest/ScreenGrid.xaml | 15
Aging.Infrastructure/Aging.Infrastructure.csproj | 34
Aging.Infrastructure/Core/ResourceManager.cs | 258
Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs | 347
Plugin.DeviceConfig/Model/DeviceConfigModel.cs | 645 +
Util.ManualDebug/StatusBtn.xaml.cs | 44
Plugin.DeviceConfig/Entity/DeviceDefinition.cs | 88
Aging.Infrastructure/Contracts/IDeviceService.cs | 81
HMI.Core/Setting/ParamSetting.cs | 11
Plugin.StandardTest/WorkOrderWin.xaml.cs | 36
Aging.Infrastructure/Contracts/ResourceKey.cs | 134
CanSimulator/CanFrame.cs | 130
Plugin.DeviceConfig/VersionManageModal.xaml | 166
Plugin.DeviceConfig/DeviceConfig.xaml | 293
fix_menu.py | 42
Plugin.StandardTest/Scanner.xaml.cs | 209
Plugin.DeviceConfig/VersionManageModal.xaml.cs | 207
Plugin.DeviceDebug/DeviceDebug.xaml.cs | 668 +
Plugin.TsSupport/ITestExecutionWrapper.cs | 2
Config/Database/database.db | 0
Plugin.StandardTest/Setting/TcpServer.cs | 49
HMI.UI/Style/PanuonResource.xaml | 9
WPFProject/PanuonWindow.xaml | 39
Plugin.StandardTest/WorkOrderWin.xaml | 24
Plugin.StandardTest/Plugin.StandardTest.csproj | 63
Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj | 33
Plugin.TsExecution/SingleControl.xaml | 1
CanSimulator/CanBusSimulator.cs | 403 +
Plugin.TestPlanEditor/EditorUI.xaml | 81
Plugin.DeviceConfig/BatchGenerateModal.xaml | 175
Plugin.DeviceConfig/DeviceConfig.xaml.cs | 611 +
Util.ManualDebug/ManualDebugWindow.xaml.cs | 114
Plugin.DeviceDebug/Plugin.DeviceDebug.csproj | 29
Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs | 222
docs/TapProject_代码结构_02.drawio.png | 0
Plugin.StandardTest/SingleControl.xaml.cs | 916 ++
Plugin.TestResult/ManualDebugWindow.xaml.cs | 6
Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml | 117
PLCHelper/IntReader.cs | 65
Aging.Infrastructure/Contracts/IDeviceDriver.cs | 114
Aging.Infrastructure/Contracts/DeviceCapability.cs | 48
CanSimulator/CanNode.cs | 311
Plugin.StandardTest/Scanner.xaml | 77
Plugin.DeviceDebug/Model/DebugSessionManager.cs | 380 +
docs/TapProject_代码结构_01.drawio.png | 0
Plugin.StandardTest/ScreenTouch.xaml | 14
Plugin.TsExecution/Properties/AssemblyInfo.cs | 4
WPFProject/App.xaml.cs | 3
Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs | 51
debug_check.py | 34
docs/TapProject_代码结构_01_预览.png | 0
PLCHelper/BoolReader.cs | 60
Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs | 63
Plugin.DeviceConfig/Plugin.DeviceConfig.csproj | 27
PLCHelper/PLCHelper.csproj | 2
Util.ManualDebug/Model/TcpServer.cs | 57
Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs | 543 +
Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs | 125
Aging.Infrastructure/Contracts/IDeviceManager.cs | 65
Plugin.StandardTest/ScreenTouchWin.xaml.cs | 67
CanSimulator/CanSimulator.csproj | 15
Aging.Infrastructure/InfrastructureBootstrapper.cs | 152
Aging.Infrastructure/Contracts/ResourceOwnerToken.cs | 84
Aging.Infrastructure/Contracts/ResourceReservation.cs | 78
Plugin.BurnIn/Plugin.BurnIn.csproj | 4
Util.ManualDebug/Model/CellConfig.cs | 14
Aging.Infrastructure/Contracts/IOperationLease.cs | 40
Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs | 44
Util.ManualDebug/OldManualDebug.xaml | 133
Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs | 197
Plugin.TestResult/ManualDebugWindow.xaml | 4
Plugin.DeviceConfig/DeviceEditModal.xaml.cs | 368 +
HMI.Core/Util/LangUtil.cs | 65
Util.ManualDebug/ManualCell.xaml.cs | 66
Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml | 176
Aging.Infrastructure/Core/DeviceManager.cs | 226
Plugin.TestPlanEditor/AssemblyInfo.cs | 12
Plugin.TsSupport/Plugin.TsSupport.csproj | 17
HMI.UnitTest/MainWindow.xaml.cs | 44
Plugin.TsExecution/TsExecution.xaml | 4
Aging.Infrastructure/Drivers/S7DriverAdapter.cs | 407 +
Plugin.StandardTest/StandardTestUI.xaml | 315
Plugin.BurnIn/TestResultView.xaml.cs | 6
check_parent.py | 38
Plugin.IntegrationTest/MainWindow.xaml | 136
Plugin.StandardTest/SingleControl.xaml | 419 +
Util.ManualDebug/OldManualDebug.xaml.cs | 368 +
Plugin.StandardTest/AssemblyInfo.cs | 12
Util.ManualDebug/ManualCell.xaml | 33
Util.ManualDebug/ManualDebug.xaml.cs | 309
Plugin.UICollection/Plugin.UICollection.csproj | 4
Plugin.StandardTest/ScannerManager.cs | 99
Plugin.TestResult/Plugin.TestResult.csproj | 4
Plugin.TsExecution/SingleControl.xaml.cs | 68
SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj | 2
Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs | 62
Plugin.IntegrationTest/Plugin.IntegrationTest.csproj | 34
Plugin.StandardTest/StandardTestUI.xaml.cs | 1906 +++++
Plugin.TsSupport/TeststandWrapper.cs | 14
Plugin.DeviceDebug/Model/DeviceDebugModel.cs | 374 +
Plugin.StandardTest/Entity/TestStep.cs | 14
Util.ManualDebug/Util.ManualDebug.csproj | 15
Aging.Infrastructure/Services/PowerSupplyService.cs | 543 +
Plugin.TsSupport/TapWrapper.cs | 405
Plugin.StandardTest/Setting/TsSetting.cs | 6
Util.ManualDebug/ManualDebugWindow.xaml | 23
HMI.UI/Resources/Font/iconfont.ttf | 0
/dev/null | 228
docs/TapProject_代码结构.drawio | 93
Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs | 218
Plugin.StandardTest/ScreenGrid.xaml.cs | 45
HMI.UnitTest/HMI.UnitTest.csproj | 15
Plugin.DeviceDebug/Entity/DebugCommand.cs | 65
Plugin.IntegrationTest/App.xaml | 36
HMI.Util/IniFileHelper.cs | 2
Plugin.TsSupport/TestAppManager.cs | 12
154 files changed, 20,141 insertions(+), 749 deletions(-)
diff --git a/Aging.Infrastructure/Aging.Infrastructure.csproj b/Aging.Infrastructure/Aging.Infrastructure.csproj
new file mode 100644
index 0000000..d9eb441
--- /dev/null
+++ b/Aging.Infrastructure/Aging.Infrastructure.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="log4net" Version="3.0.4" />
+ <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="HslCommunication">
+ <HintPath>..\Libs\HslCommunication.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
+ <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
+ <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj" />
+ <ProjectReference Include="..\PLCHelper\PLCHelper.csproj" />
+ <ProjectReference Include="..\S7Driver\S7Driver.csproj" />
+ <ProjectReference Include="..\ModBusTcpController\ModBusTcpController.csproj" />
+ <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Aging.Infrastructure/Contracts/DeviceCapability.cs b/Aging.Infrastructure/Contracts/DeviceCapability.cs
new file mode 100644
index 0000000..3b363c1
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/DeviceCapability.cs
@@ -0,0 +1,48 @@
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璁惧鑳藉姏鏋氫妇锛屾爣璇嗙‖浠惰澶囨敮鎸佺殑鍔熻兘绫诲瀷
+ /// </summary>
+ public enum DeviceCapability
+ {
+ /// <summary>
+ /// 鐢垫簮璁惧
+ /// </summary>
+ PowerSupply,
+
+ /// <summary>
+ /// CAN鎬荤嚎璁惧
+ /// </summary>
+ CAN,
+
+ /// <summary>
+ /// LIN鎬荤嚎璁惧
+ /// </summary>
+ LIN,
+
+ /// <summary>
+ /// PLC鏁板瓧IO璁惧
+ /// </summary>
+ PLC_IO,
+
+ /// <summary>
+ /// 娓╃璁惧
+ /// </summary>
+ Chamber,
+
+ /// <summary>
+ /// 鏁版嵁閲囬泦璁惧
+ /// </summary>
+ DAQ,
+
+ /// <summary>
+ /// 涓插彛璁惧
+ /// </summary>
+ Serial,
+
+ /// <summary>
+ /// 閫氱敤璁惧
+ /// </summary>
+ Generic
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/IDeviceDriver.cs b/Aging.Infrastructure/Contracts/IDeviceDriver.cs
new file mode 100644
index 0000000..c23e64c
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/IDeviceDriver.cs
@@ -0,0 +1,114 @@
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璁惧椹卞姩鐘舵�佷俊鎭�
+ /// </summary>
+ public sealed class DeviceDriverStatus
+ {
+ /// <summary>
+ /// 璁惧鏄惁宸茶繛鎺�
+ /// </summary>
+ public bool IsConnected { get; set; }
+
+ /// <summary>
+ /// 璁惧鏄惁鍋ュ悍锛堟棤鏁呴殰锛�
+ /// </summary>
+ public bool IsHealthy { get; set; }
+
+ /// <summary>
+ /// 鏈�鍚庝竴娆¢敊璇俊鎭�
+ /// </summary>
+ public string LastError { get; set; } = "";
+
+ /// <summary>
+ /// 鏈�鍚庝竴娆¢敊璇彂鐢熺殑鏃堕棿
+ /// </summary>
+ public DateTime? LastErrorTime { get; set; }
+
+ /// <summary>
+ /// 閫氫俊寤惰繜锛堟绉掞級
+ /// </summary>
+ public double LatencyMs { get; set; }
+
+ /// <summary>
+ /// 閲嶈繛娆℃暟
+ /// </summary>
+ public int ReconnectCount { get; set; }
+ }
+
+ /// <summary>
+ /// 璁惧杩炴帴鐘舵�佸彉鍖栦簨浠跺弬鏁�
+ /// </summary>
+ public sealed class DeviceConnectionEventArgs : EventArgs
+ {
+ /// <summary>
+ /// 璁惧鏍囪瘑绗�
+ /// </summary>
+ public string DeviceId { get; set; } = "";
+
+ /// <summary>
+ /// 鏄惁宸茶繛鎺�
+ /// </summary>
+ public bool IsConnected { get; set; }
+
+ /// <summary>
+ /// 鐘舵�佸彉鍖栧師鍥狅紙鍙�夛級
+ /// </summary>
+ public string? Reason { get; set; }
+ }
+
+ /// <summary>
+ /// 璁惧椹卞姩缁熶竴鎺ュ彛銆�
+ /// 鎵�鏈夌‖浠惰澶囬┍鍔ㄥ繀椤诲疄鐜版鎺ュ彛锛屾彁渚涜繛鎺ャ�佹柇寮�銆佸懡浠ゅ彂閫佸拰鏌ヨ鍔熻兘銆�
+ /// </summary>
+ public interface IDeviceDriver : IDisposable
+ {
+ /// <summary>
+ /// 璁惧鍞竴鏍囪瘑绗�
+ /// </summary>
+ string DeviceId { get; }
+
+ /// <summary>
+ /// 璁惧褰撳墠鏄惁宸茶繛鎺�
+ /// </summary>
+ bool IsConnected { get; }
+
+ /// <summary>
+ /// 寮傛杩炴帴璁惧
+ /// </summary>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ Task ConnectAsync(CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛鏂紑璁惧杩炴帴
+ /// </summary>
+ Task DisconnectAsync();
+
+ /// <summary>
+ /// 鍙戦�佸懡浠ゅ埌璁惧
+ /// </summary>
+ /// <param name="command">鍛戒护瀛楃涓�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>璁惧鍝嶅簲瀛楃涓�</returns>
+ Task<string> SendCommandAsync(string command, CancellationToken ct);
+
+ /// <summary>
+ /// 鍚戣澶囧彂閫佹煡璇㈠苟鑾峰彇鍝嶅簲
+ /// </summary>
+ /// <param name="query">鏌ヨ瀛楃涓�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>璁惧鏌ヨ鍝嶅簲瀛楃涓�</returns>
+ Task<string> QueryAsync(string query, CancellationToken ct);
+
+ /// <summary>
+ /// 鑾峰彇璁惧褰撳墠鐘舵�佷俊鎭�
+ /// </summary>
+ /// <returns>璁惧椹卞姩鐘舵��</returns>
+ DeviceDriverStatus GetStatus();
+
+ /// <summary>
+ /// 璁惧杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/IDeviceManager.cs b/Aging.Infrastructure/Contracts/IDeviceManager.cs
new file mode 100644
index 0000000..680a9c3
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/IDeviceManager.cs
@@ -0,0 +1,65 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璁惧绠$悊鎺ュ彛銆�
+ /// 鎻愪緵璁惧椹卞姩鐨勬敞鍐屻�佽繛鎺ョ鐞嗗拰鐘舵�佹煡璇㈠姛鑳姐��
+ /// </summary>
+ public interface IDeviceManager
+ {
+ /// <summary>
+ /// 鏍规嵁璁惧ID鑾峰彇璁惧椹卞姩瀹炰緥
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <returns>璁惧椹卞姩瀹炰緥锛屼笉瀛樺湪鏃惰繑鍥� null</returns>
+ IDeviceDriver? GetDriver(string deviceId);
+
+ /// <summary>
+ /// 鑾峰彇璁惧褰撳墠鐘舵��
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <returns>璁惧椹卞姩鐘舵��</returns>
+ DeviceDriverStatus GetStatus(string deviceId);
+
+ /// <summary>
+ /// 娉ㄥ唽璁惧椹卞姩
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="driver">璁惧椹卞姩瀹炰緥</param>
+ void RegisterDriver(string deviceId, IDeviceDriver driver);
+
+ /// <summary>
+ /// 娉ㄩ攢璁惧椹卞姩
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ void UnregisterDriver(string deviceId);
+
+ /// <summary>
+ /// 寮傛杩炴帴璁惧
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ Task ConnectAsync(string deviceId, CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛鏂紑璁惧杩炴帴
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ Task DisconnectAsync(string deviceId);
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈夊凡娉ㄥ唽璁惧鐨処D鍒楄〃
+ /// </summary>
+ /// <returns>璁惧ID鍙鍒楄〃</returns>
+ IReadOnlyList<string> GetRegisteredDevices();
+
+ /// <summary>
+ /// 妫�鏌ヨ澶囨槸鍚﹀凡娉ㄥ唽
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <returns>璁惧鏄惁宸叉敞鍐�</returns>
+ bool IsDeviceRegistered(string deviceId);
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/IDeviceService.cs b/Aging.Infrastructure/Contracts/IDeviceService.cs
new file mode 100644
index 0000000..8d7b66e
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/IDeviceService.cs
@@ -0,0 +1,81 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璁惧鍛戒护鎵ц缁撴灉
+ /// </summary>
+ public sealed class DeviceCommandResult
+ {
+ /// <summary>
+ /// 鍛戒护鏄惁鎵ц鎴愬姛
+ /// </summary>
+ public bool Success { get; set; }
+
+ /// <summary>
+ /// 璁惧鍘熷鍝嶅簲瀛楃涓�
+ /// </summary>
+ public string? RawResponse { get; set; }
+
+ /// <summary>
+ /// 鍥炶鍊硷紙瑙f瀽鍚庣殑鏈夋晥鏁版嵁锛�
+ /// </summary>
+ public string? Readback { get; set; }
+
+ /// <summary>
+ /// 閿欒浠g爜
+ /// </summary>
+ public string? ErrorCode { get; set; }
+
+ /// <summary>
+ /// 閿欒淇℃伅
+ /// </summary>
+ public string? ErrorMessage { get; set; }
+
+ /// <summary>
+ /// 鍛戒护鎵ц寤惰繜锛堟绉掞級
+ /// </summary>
+ public double LatencyMs { get; set; }
+ }
+
+ /// <summary>
+ /// 璁惧鏈嶅姟鎺ュ彛銆�
+ /// 鎻愪緵鍩轰簬璧勬簮棰勭害鐨勫懡浠ゆ墽琛屽拰瀹夊叏鍏虫満鍔熻兘锛屽崗璋冭祫婧愮鐞嗗拰璁惧椹卞姩銆�
+ /// </summary>
+ public interface IDeviceService
+ {
+ /// <summary>
+ /// 寮傛鎵ц璁惧鍛戒护銆�
+ /// 闇�瑕佹湁鏁堢殑璧勬簮棰勭害鎵嶈兘鎵ц銆�
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="command">鍛戒护瀛楃涓�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鍛戒护鎵ц缁撴灉</returns>
+ Task<DeviceCommandResult> ExecuteCommandAsync(
+ string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛鎵ц璁惧鏌ヨ銆�
+ /// 闇�瑕佹湁鏁堢殑璧勬簮棰勭害鎵嶈兘鎵ц銆�
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="query">鏌ヨ瀛楃涓�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鍛戒护鎵ц缁撴灉</returns>
+ Task<DeviceCommandResult> ExecuteQueryAsync(
+ string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛瀹夊叏鍏抽棴璁惧銆�
+ /// 鎵ц璁惧鐗瑰畾鐨勫叧鏈哄簭鍒楋紝纭繚璁惧澶勪簬瀹夊叏鐘舵�併��
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct);
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/IOperationLease.cs b/Aging.Infrastructure/Contracts/IOperationLease.cs
new file mode 100644
index 0000000..a8611f6
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/IOperationLease.cs
@@ -0,0 +1,40 @@
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 鐭湡鎿嶄綔绉熺害鎺ュ彛銆�
+ /// 鍦ㄦ墽琛屽叿浣撶‖浠舵搷浣滃墠鑾峰彇锛岀‘淇濇搷浣滄湡闂磋祫婧愪笉琚姠鍗犮��
+ /// 鏀寔鍚屾鍜屽紓姝ラ噴鏀俱��
+ /// </summary>
+ public interface IOperationLease : IDisposable, IAsyncDisposable
+ {
+ /// <summary>
+ /// 绉熺害鍞竴鏍囪瘑
+ /// </summary>
+ string LeaseId { get; }
+
+ /// <summary>
+ /// 绉熺害鍏宠仈鐨勮祫婧愰敭
+ /// </summary>
+ ResourceKey Resource { get; }
+
+ /// <summary>
+ /// 绉熺害鎵�鏈夎�呬护鐗�
+ /// </summary>
+ ResourceOwnerToken Owner { get; }
+
+ /// <summary>
+ /// 绉熺害鑾峰彇鏃堕棿
+ /// </summary>
+ DateTime AcquiredAt { get; }
+
+ /// <summary>
+ /// 绉熺害杩囨湡鏃堕棿锛堝彲閫夛級
+ /// </summary>
+ DateTime? ExpiresAt { get; }
+
+ /// <summary>
+ /// 绉熺害鏄惁浠嶇劧鏈夋晥
+ /// </summary>
+ bool IsValid { get; }
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/IResourceManager.cs b/Aging.Infrastructure/Contracts/IResourceManager.cs
new file mode 100644
index 0000000..3d3b09a
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/IResourceManager.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璧勬簮鍐茬獊寮傚父銆�
+ /// 褰撳皾璇曢绾﹀凡琚叾浠栨墍鏈夎�呭崰鐢ㄧ殑璧勬簮鏃舵姏鍑恒��
+ /// </summary>
+ public sealed class ResourceConflictException : Exception
+ {
+ /// <summary>
+ /// 鍒濆鍖栬祫婧愬啿绐佸紓甯稿疄渚�
+ /// </summary>
+ /// <param name="message">鍐茬獊鎻忚堪淇℃伅</param>
+ public ResourceConflictException(string message) : base(message)
+ {
+ }
+
+ /// <summary>
+ /// 鍒濆鍖栬祫婧愬啿绐佸紓甯稿疄渚�
+ /// </summary>
+ /// <param name="message">鍐茬獊鎻忚堪淇℃伅</param>
+ /// <param name="innerException">鍐呴儴寮傚父</param>
+ public ResourceConflictException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+
+ /// <summary>
+ /// 璧勬簮绠$悊鎺ュ彛銆�
+ /// 鎻愪緵纭欢璧勬簮鐨勯绾︺�佺绾﹁幏鍙栧拰閲婃斁鍔熻兘锛岀‘淇濊祫婧愪笉琚涓細璇濆悓鏃跺崰鐢ㄣ��
+ /// </summary>
+ public interface IResourceManager
+ {
+ /// <summary>
+ /// 寮傛棰勭害璧勬簮銆�
+ /// 鍒涘缓闀挎湡棰勭害锛岄樆姝㈠叾浠栨墍鏈夎�呬娇鐢ㄨ璧勬簮銆�
+ /// </summary>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>璧勬簮棰勭害瀹炰綋</returns>
+ /// <exception cref="ResourceConflictException">璧勬簮宸茶鍏朵粬鎵�鏈夎�呴绾︽椂鎶涘嚭</exception>
+ Task<ResourceReservation> ReserveAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛鑾峰彇鎿嶄綔绉熺害銆�
+ /// 鍦ㄦ墽琛屽叿浣撶‖浠舵搷浣滃墠璋冪敤锛岀‘淇濇搷浣滄湡闂磋祫婧愪笉琚姠鍗犮��
+ /// </summary>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鎿嶄綔绉熺害</returns>
+ /// <exception cref="InvalidOperationException">璧勬簮鏈绾︽垨鎵�鏈夎�呬笉鍖归厤鏃舵姏鍑�</exception>
+ Task<IOperationLease> AcquireOperationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
+
+ /// <summary>
+ /// 寮傛閲婃斁璧勬簮棰勭害銆�
+ /// 鍙栨秷瀵硅祫婧愮殑鍗犵敤锛屽厑璁稿叾浠栨墍鏈夎�呴绾︺��
+ /// </summary>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ Task ReleaseReservationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct);
+
+ /// <summary>
+ /// 妫�鏌ヨ祫婧愭槸鍚﹀凡琚绾�
+ /// </summary>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="reservation">杈撳嚭棰勭害瀹炰綋锛堝鏋滃凡棰勭害锛�</param>
+ /// <returns>璧勬簮鏄惁宸茶棰勭害</returns>
+ bool IsReserved(ResourceKey resource, out ResourceReservation? reservation);
+
+ /// <summary>
+ /// 楠岃瘉鎵�鏈夎�呬护鐗屾槸鍚﹀尮閰嶆寚瀹氳祫婧愮殑棰勭害
+ /// </summary>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <returns>鎵�鏈夎�呮槸鍚﹀尮閰�</returns>
+ bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource);
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/ResourceKey.cs b/Aging.Infrastructure/Contracts/ResourceKey.cs
new file mode 100644
index 0000000..c250f51
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/ResourceKey.cs
@@ -0,0 +1,134 @@
+using System;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 璧勬簮閿�煎璞★紝鍞竴鏍囪瘑涓�涓彲绠$悊鐨勭‖浠惰祫婧愩��
+ /// 鏍煎紡: "DeviceId:Channel"锛屽 "PSU024:CH1"銆�"CAN_CARD01:CH2"銆�"PLC01:OUTPUT_GROUP_A"
+ /// </summary>
+ public sealed class ResourceKey : IEquatable<ResourceKey>
+ {
+ /// <summary>
+ /// 璁惧鏍囪瘑绗�
+ /// </summary>
+ public string DeviceId { get; }
+
+ /// <summary>
+ /// 閫氶亾鏍囪瘑锛堝彲閫夛級
+ /// </summary>
+ public string? Channel { get; }
+
+ /// <summary>
+ /// 璧勬簮绫诲瀷锛圥owerSupply/CAN/PLC/Chamber绛夛級
+ /// </summary>
+ public string ResourceType { get; }
+
+ /// <summary>
+ /// 鍒濆鍖栬祫婧愰敭瀹炰緥
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="resourceType">璧勬簮绫诲瀷</param>
+ /// <param name="channel">閫氶亾鏍囪瘑锛堝彲閫夛級</param>
+ public ResourceKey(string deviceId, string resourceType, string? channel = null)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ ResourceType = resourceType ?? throw new ArgumentNullException(nameof(resourceType));
+ Channel = channel;
+ }
+
+ /// <summary>
+ /// 浠庡瓧绗︿覆瑙f瀽璧勬簮閿紝鏍煎紡涓� "DeviceId:Channel"
+ /// </summary>
+ /// <param name="key">璧勬簮閿瓧绗︿覆</param>
+ /// <returns>瑙f瀽鍚庣殑璧勬簮閿疄渚�</returns>
+ /// <exception cref="ArgumentException">鏍煎紡鏃犳晥鏃舵姏鍑�</exception>
+ public static ResourceKey FromString(string key)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ throw new ArgumentException("璧勬簮閿笉鑳戒负绌�", nameof(key));
+
+ var parts = key.Split(':');
+ if (parts.Length < 2)
+ throw new ArgumentException($"鏃犳晥鐨勮祫婧愰敭鏍煎紡: '{key}'锛屾湡鏈涙牸寮忎负 'DeviceId:Channel'");
+
+ var deviceId = parts[0].Trim();
+ var channel = string.Join(":", parts.Skip(1)).Trim();
+
+ if (string.IsNullOrEmpty(deviceId))
+ throw new ArgumentException($"鏃犳晥鐨勮祫婧愰敭鏍煎紡: '{key}'锛孌eviceId 涓嶈兘涓虹┖");
+
+ // 鏍规嵁璁惧ID鍓嶇紑鎺ㄦ柇璧勬簮绫诲瀷
+ var resourceType = InferResourceType(deviceId);
+
+ return new ResourceKey(deviceId, resourceType, string.IsNullOrEmpty(channel) ? null : channel);
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎿嶄綔璧勬簮閿紝鐢ㄤ簬鐗╃悊浼氳瘽閿�
+ /// </summary>
+ public string OperationResourceKey => $"{DeviceId}:SESSION";
+
+ /// <summary>
+ /// 鏍规嵁璁惧ID鍓嶇紑鎺ㄦ柇璧勬簮绫诲瀷
+ /// </summary>
+ private static string InferResourceType(string deviceId)
+ {
+ var upper = deviceId.ToUpperInvariant();
+ if (upper.StartsWith("PSU") || upper.StartsWith("POWER"))
+ return "PowerSupply";
+ if (upper.StartsWith("CAN"))
+ return "CAN";
+ if (upper.StartsWith("PLC"))
+ return "PLC";
+ if (upper.StartsWith("CHAMBER") || upper.StartsWith("TEMP"))
+ return "Chamber";
+ if (upper.StartsWith("DAQ"))
+ return "DAQ";
+ if (upper.StartsWith("SERIAL") || upper.StartsWith("COM"))
+ return "Serial";
+ return "Generic";
+ }
+
+ /// <inheritdoc/>
+ public bool Equals(ResourceKey? other)
+ {
+ if (other is null) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return string.Equals(DeviceId, other.DeviceId, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(Channel, other.Channel, StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(ResourceType, other.ResourceType, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <inheritdoc/>
+ public override bool Equals(object? obj) => Equals(obj as ResourceKey);
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + (DeviceId?.ToUpperInvariant().GetHashCode() ?? 0);
+ hash = hash * 31 + (Channel?.ToUpperInvariant().GetHashCode() ?? 0);
+ hash = hash * 31 + (ResourceType?.ToUpperInvariant().GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+
+ /// <inheritdoc/>
+ public override string ToString() =>
+ string.IsNullOrEmpty(Channel) ? DeviceId : $"{DeviceId}:{Channel}";
+
+ /// <summary>
+ /// 鐩哥瓑杩愮畻绗﹂噸杞�
+ /// </summary>
+ public static bool operator ==(ResourceKey? left, ResourceKey? right) =>
+ Equals(left, right);
+
+ /// <summary>
+ /// 涓嶇瓑杩愮畻绗﹂噸杞�
+ /// </summary>
+ public static bool operator !=(ResourceKey? left, ResourceKey? right) =>
+ !Equals(left, right);
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/ResourceOwnerToken.cs b/Aging.Infrastructure/Contracts/ResourceOwnerToken.cs
new file mode 100644
index 0000000..3b7b9bf
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/ResourceOwnerToken.cs
@@ -0,0 +1,84 @@
+using System;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 鎵�鏈夎�呯被鍨嬫灇涓�
+ /// </summary>
+ public enum OwnerType
+ {
+ /// <summary>
+ /// 鐢熶骇妯″紡锛堟甯歌�佸寲娴嬭瘯娴佺▼锛�
+ /// </summary>
+ Production,
+
+ /// <summary>
+ /// 缁存姢妯″紡锛堣皟璇�/缁存姢浼氳瘽锛�
+ /// </summary>
+ Maintenance
+ }
+
+ /// <summary>
+ /// 鎵�鏈夎�呬护鐗岋紝鏍囪瘑璧勬簮棰勭害鐨勫綊灞炪��
+ /// 鐢ㄤ簬鍖哄垎涓嶅悓浼氳瘽瀵圭‖浠惰祫婧愮殑鎵�鏈夋潈銆�
+ /// </summary>
+ public sealed class ResourceOwnerToken
+ {
+ /// <summary>
+ /// 浠ょ墝鍞竴鏍囪瘑
+ /// </summary>
+ public string TokenId { get; }
+
+ /// <summary>
+ /// 鎵�鏈夎�呯被鍨嬶紙鐢熶骇/缁存姢锛�
+ /// </summary>
+ public OwnerType OwnerType { get; }
+
+ /// <summary>
+ /// 鎵�鏈夎�呮爣璇嗭紙RunID 鎴� DebugSessionID锛�
+ /// </summary>
+ public string OwnerId { get; }
+
+ /// <summary>
+ /// 涓栦唬鍙凤紝姣忔鏂伴绾﹂�掑锛岄槻姝㈤噸鏀炬敾鍑�
+ /// </summary>
+ public long Generation { get; set; }
+
+ /// <summary>
+ /// 浠ょ墝绛惧彂鏃堕棿
+ /// </summary>
+ public DateTime IssuedAt { get; }
+
+ /// <summary>
+ /// 浠ょ墝杩囨湡鏃堕棿锛堝彲閫夛級
+ /// </summary>
+ public DateTime? ExpiresAt { get; }
+
+ /// <summary>
+ /// 鍒濆鍖栨墍鏈夎�呬护鐗屽疄渚�
+ /// </summary>
+ /// <param name="ownerType">鎵�鏈夎�呯被鍨�</param>
+ /// <param name="ownerId">鎵�鏈夎�呮爣璇�</param>
+ /// <param name="expiresAt">杩囨湡鏃堕棿锛堝彲閫夛級</param>
+ public ResourceOwnerToken(OwnerType ownerType, string ownerId, DateTime? expiresAt = null)
+ {
+ TokenId = Guid.NewGuid().ToString();
+ OwnerType = ownerType;
+ OwnerId = ownerId ?? throw new ArgumentNullException(nameof(ownerId));
+ Generation = 0;
+ IssuedAt = DateTime.UtcNow;
+ ExpiresAt = expiresAt;
+ }
+
+ /// <summary>
+ /// 妫�鏌ヤ护鐗屾槸鍚﹀凡杩囨湡
+ /// </summary>
+ /// <returns>杩囨湡杩斿洖 true锛涙湭璁剧疆杩囨湡鏃堕棿鎴栧皻鏈繃鏈熻繑鍥� false</returns>
+ public bool IsExpired()
+ {
+ if (!ExpiresAt.HasValue)
+ return false;
+ return DateTime.UtcNow > ExpiresAt.Value;
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Contracts/ResourceReservation.cs b/Aging.Infrastructure/Contracts/ResourceReservation.cs
new file mode 100644
index 0000000..994ed54
--- /dev/null
+++ b/Aging.Infrastructure/Contracts/ResourceReservation.cs
@@ -0,0 +1,78 @@
+using System;
+
+namespace Aging.Infrastructure.Contracts
+{
+ /// <summary>
+ /// 棰勭害鐘舵�佹灇涓�
+ /// </summary>
+ public enum ReservationState
+ {
+ /// <summary>
+ /// 娲昏穬鐘舵��
+ /// </summary>
+ Active,
+
+ /// <summary>
+ /// 宸查噴鏀�
+ /// </summary>
+ Released,
+
+ /// <summary>
+ /// 鍐茬獊鐘舵��
+ /// </summary>
+ Conflicted
+ }
+
+ /// <summary>
+ /// 闀挎湡棰勭害瀹炰綋锛岃〃绀哄鏌愪釜纭欢璧勬簮鐨勬寔涔呭崰鐢ㄣ��
+ /// 棰勭害鏈熼棿锛屽叾浠栨墍鏈夎�呮棤娉曡幏鍙栬璧勬簮銆�
+ /// </summary>
+ public sealed class ResourceReservation
+ {
+ /// <summary>
+ /// 棰勭害鍞竴鏍囪瘑
+ /// </summary>
+ public string ReservationId { get; }
+
+ /// <summary>
+ /// 琚绾︾殑璧勬簮閿�
+ /// </summary>
+ public ResourceKey Resource { get; }
+
+ /// <summary>
+ /// 棰勭害鎵�鏈夎�呬护鐗�
+ /// </summary>
+ public ResourceOwnerToken Owner { get; }
+
+ /// <summary>
+ /// 棰勭害鐘舵��
+ /// </summary>
+ public ReservationState State { get; set; }
+
+ /// <summary>
+ /// 棰勭害鍒涘缓鏃堕棿
+ /// </summary>
+ public DateTime ReservedAt { get; }
+
+ /// <summary>
+ /// 棰勭害鎻忚堪锛堝彲閫夛級
+ /// </summary>
+ public string? Description { get; set; }
+
+ /// <summary>
+ /// 鍒濆鍖栬祫婧愰绾﹀疄渚�
+ /// </summary>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="description">棰勭害鎻忚堪锛堝彲閫夛級</param>
+ public ResourceReservation(ResourceKey resource, ResourceOwnerToken owner, string? description = null)
+ {
+ ReservationId = Guid.NewGuid().ToString();
+ Resource = resource ?? throw new ArgumentNullException(nameof(resource));
+ Owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ State = ReservationState.Active;
+ ReservedAt = DateTime.UtcNow;
+ Description = description;
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Core/DeviceManager.cs b/Aging.Infrastructure/Core/DeviceManager.cs
new file mode 100644
index 0000000..7622a94
--- /dev/null
+++ b/Aging.Infrastructure/Core/DeviceManager.cs
@@ -0,0 +1,226 @@
+using System.Collections.Concurrent;
+using Aging.Infrastructure.Contracts;
+using log4net;
+
+namespace Aging.Infrastructure.Core
+{
+ /// <summary>
+ /// 璁惧绠$悊瀹炵幇銆�
+ /// 鎻愪緵璁惧椹卞姩鐨勬敞鍐屻�佽繛鎺ョ鐞嗗拰鐘舵�佹煡璇㈠姛鑳姐��
+ /// 浣跨敤 ConcurrentDictionary 绠$悊椹卞姩瀹炰緥鍜岀紦瀛樼姸鎬併��
+ /// </summary>
+ public sealed class DeviceManager : IDeviceManager
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceManager));
+
+ /// <summary>
+ /// 璁惧椹卞姩瀛楀吀锛坘ey = deviceId锛�
+ /// </summary>
+ private readonly ConcurrentDictionary<string, IDeviceDriver> _drivers = new();
+
+ /// <summary>
+ /// 璁惧鐘舵�佺紦瀛樺瓧鍏革紙key = deviceId锛�
+ /// </summary>
+ private readonly ConcurrentDictionary<string, DeviceDriverStatus> _statusCache = new();
+
+ /// <inheritdoc/>
+ public void RegisterDriver(string deviceId, IDeviceDriver driver)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+ if (driver == null) throw new ArgumentNullException(nameof(driver));
+
+ if (_drivers.ContainsKey(deviceId))
+ {
+ Log.Warn($"[DeviceManager] 璁惧 {deviceId} 宸叉敞鍐岋紝灏嗚鐩栫幇鏈夐┍鍔�");
+ UnregisterDriver(deviceId);
+ }
+
+ _drivers[deviceId] = driver;
+
+ // 鍒濆鍖栫姸鎬佺紦瀛�
+ var initialStatus = driver.GetStatus();
+ _statusCache[deviceId] = initialStatus;
+
+ // 璁㈤槄杩炴帴鐘舵�佸彉鍖栦簨浠�
+ driver.ConnectionChanged += OnConnectionChanged;
+
+ Log.Info($"[DeviceManager] 璁惧 {deviceId} 椹卞姩娉ㄥ唽鎴愬姛锛屽綋鍓嶈繛鎺ョ姸鎬�: {initialStatus.IsConnected}");
+ }
+
+ /// <inheritdoc/>
+ public void UnregisterDriver(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+
+ if (_drivers.TryRemove(deviceId, out var driver))
+ {
+ // 鍙栨秷浜嬩欢璁㈤槄
+ driver.ConnectionChanged -= OnConnectionChanged;
+
+ // 娓呯悊鐘舵�佺紦瀛�
+ _statusCache.TryRemove(deviceId, out _);
+
+ Log.Info($"[DeviceManager] 璁惧 {deviceId} 椹卞姩宸叉敞閿�");
+ }
+ else
+ {
+ Log.Warn($"[DeviceManager] 灏濊瘯娉ㄩ攢涓嶅瓨鍦ㄧ殑璁惧: {deviceId}");
+ }
+ }
+
+ /// <inheritdoc/>
+ public IDeviceDriver? GetDriver(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+
+ _drivers.TryGetValue(deviceId, out var driver);
+ return driver;
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+
+ if (_statusCache.TryGetValue(deviceId, out var status))
+ {
+ return status;
+ }
+
+ // 濡傛灉缂撳瓨涓病鏈夛紝杩斿洖榛樿鐘舵��
+ Log.Warn($"[DeviceManager] 璁惧 {deviceId} 鐘舵�佺紦瀛樹笉瀛樺湪锛岃繑鍥為粯璁ょ姸鎬�");
+ return new DeviceDriverStatus
+ {
+ IsConnected = false,
+ IsHealthy = false,
+ LastError = "璁惧鏈敞鍐屾垨鐘舵�佹湭鍒濆鍖�"
+ };
+ }
+
+ /// <inheritdoc/>
+ public async Task ConnectAsync(string deviceId, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+
+ if (!_drivers.TryGetValue(deviceId, out var driver))
+ {
+ throw new InvalidOperationException($"璁惧 {deviceId} 鏈敞鍐岄┍鍔�");
+ }
+
+ try
+ {
+ Log.Info($"[DeviceManager] 姝e湪杩炴帴璁惧: {deviceId}");
+ await driver.ConnectAsync(ct).ConfigureAwait(false);
+
+ // 鏇存柊鐘舵�佺紦瀛�
+ _statusCache[deviceId] = driver.GetStatus();
+
+ Log.Info($"[DeviceManager] 璁惧 {deviceId} 杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ // 鏇存柊閿欒鐘舵��
+ if (_statusCache.TryGetValue(deviceId, out var status))
+ {
+ status.IsConnected = false;
+ status.IsHealthy = false;
+ status.LastError = ex.Message;
+ status.LastErrorTime = DateTime.UtcNow;
+ _statusCache[deviceId] = status;
+ }
+
+ Log.Error($"[DeviceManager] 璁惧 {deviceId} 杩炴帴澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task DisconnectAsync(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+
+ if (!_drivers.TryGetValue(deviceId, out var driver))
+ {
+ Log.Warn($"[DeviceManager] 灏濊瘯鏂紑鏈敞鍐岀殑璁惧: {deviceId}");
+ return;
+ }
+
+ try
+ {
+ Log.Info($"[DeviceManager] 姝e湪鏂紑璁惧: {deviceId}");
+ await driver.DisconnectAsync().ConfigureAwait(false);
+
+ // 鏇存柊鐘舵�佺紦瀛�
+ _statusCache[deviceId] = driver.GetStatus();
+
+ Log.Info($"[DeviceManager] 璁惧 {deviceId} 宸叉柇寮�杩炴帴");
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[DeviceManager] 璁惧 {deviceId} 鏂紑杩炴帴澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public IReadOnlyList<string> GetRegisteredDevices()
+ {
+ return _drivers.Keys.ToList().AsReadOnly();
+ }
+
+ /// <inheritdoc/>
+ public bool IsDeviceRegistered(string deviceId)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ return false;
+
+ return _drivers.ContainsKey(deviceId);
+ }
+
+ /// <summary>
+ /// 澶勭悊璁惧杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ /// <param name="sender">浜嬩欢鍙戦�佽��</param>
+ /// <param name="e">浜嬩欢鍙傛暟</param>
+ private void OnConnectionChanged(object? sender, DeviceConnectionEventArgs e)
+ {
+ var deviceId = e.DeviceId;
+
+ if (string.IsNullOrWhiteSpace(deviceId))
+ {
+ Log.Warn("[DeviceManager] 鏀跺埌杩炴帴鍙樺寲浜嬩欢浣� DeviceId 涓虹┖");
+ return;
+ }
+
+ if (_statusCache.TryGetValue(deviceId, out var status))
+ {
+ status.IsConnected = e.IsConnected;
+ if (!e.IsConnected)
+ {
+ status.IsHealthy = false;
+ if (!string.IsNullOrEmpty(e.Reason))
+ {
+ status.LastError = e.Reason;
+ status.LastErrorTime = DateTime.UtcNow;
+ }
+ }
+ else
+ {
+ status.IsHealthy = true;
+ status.LastError = "";
+ }
+
+ _statusCache[deviceId] = status;
+
+ Log.Info($"[DeviceManager] 璁惧 {deviceId} 杩炴帴鐘舵�佸彉鍖�: {(e.IsConnected ? "宸茶繛鎺�" : "宸叉柇寮�")}" +
+ (string.IsNullOrEmpty(e.Reason) ? "" : $", 鍘熷洜: {e.Reason}"));
+ }
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Core/OperationLease.cs b/Aging.Infrastructure/Core/OperationLease.cs
new file mode 100644
index 0000000..3d41e55
--- /dev/null
+++ b/Aging.Infrastructure/Core/OperationLease.cs
@@ -0,0 +1,82 @@
+using Aging.Infrastructure.Contracts;
+
+namespace Aging.Infrastructure.Core
+{
+ /// <summary>
+ /// 鎿嶄綔绉熺害瀹炵幇銆�
+ /// 鎻愪緵鐭湡璧勬簮鍗犵敤淇濊瘉锛屾敮鎸佸悓姝ュ拰寮傛閲婃斁銆�
+ /// 閲婃斁鏃堕�氳繃鍥炶皟閫氱煡 ResourceManager銆�
+ /// </summary>
+ public sealed class OperationLease : IOperationLease
+ {
+ private readonly Action? _onRelease;
+ private bool _disposed;
+
+ /// <inheritdoc/>
+ public string LeaseId { get; }
+
+ /// <inheritdoc/>
+ public ResourceKey Resource { get; }
+
+ /// <inheritdoc/>
+ public ResourceOwnerToken Owner { get; }
+
+ /// <inheritdoc/>
+ public DateTime AcquiredAt { get; }
+
+ /// <inheritdoc/>
+ public DateTime? ExpiresAt { get; }
+
+ /// <inheritdoc/>
+ public bool IsValid => !_disposed && !IsExpired();
+
+ /// <summary>
+ /// 鍒濆鍖栨搷浣滅绾﹀疄渚�
+ /// </summary>
+ /// <param name="resource">璧勬簮閿�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="onRelease">閲婃斁鏃剁殑鍥炶皟閫氱煡</param>
+ /// <param name="leaseDurationSeconds">绉熺害鏈夋晥鏈燂紙绉掞級锛岄粯璁�30绉�</param>
+ public OperationLease(
+ ResourceKey resource,
+ ResourceOwnerToken owner,
+ Action? onRelease = null,
+ int leaseDurationSeconds = 30)
+ {
+ LeaseId = Guid.NewGuid().ToString();
+ Resource = resource ?? throw new ArgumentNullException(nameof(resource));
+ Owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ AcquiredAt = DateTime.UtcNow;
+ ExpiresAt = leaseDurationSeconds > 0
+ ? AcquiredAt.AddSeconds(leaseDurationSeconds)
+ : null;
+ _onRelease = onRelease;
+ _disposed = false;
+ }
+
+ /// <summary>
+ /// 妫�鏌ョ绾︽槸鍚﹀凡杩囨湡
+ /// </summary>
+ private bool IsExpired()
+ {
+ if (!ExpiresAt.HasValue)
+ return false;
+ return DateTime.UtcNow > ExpiresAt.Value;
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ _onRelease?.Invoke();
+ }
+
+ /// <inheritdoc/>
+ public ValueTask DisposeAsync()
+ {
+ Dispose();
+ return default;
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Core/ResourceManager.cs b/Aging.Infrastructure/Core/ResourceManager.cs
new file mode 100644
index 0000000..68f4c2b
--- /dev/null
+++ b/Aging.Infrastructure/Core/ResourceManager.cs
@@ -0,0 +1,258 @@
+using System.Collections.Concurrent;
+using Aging.Infrastructure.Contracts;
+using log4net;
+
+namespace Aging.Infrastructure.Core
+{
+ /// <summary>
+ /// 璧勬簮绠$悊瀹炵幇銆�
+ /// 鎻愪緵绾跨▼瀹夊叏鐨勮祫婧愰绾︺�佺绾﹁幏鍙栧拰閲婃斁鍔熻兘銆�
+ /// 浣跨敤 ConcurrentDictionary 瀛樺偍娲昏穬棰勭害锛屼娇鐢� SemaphoreSlim 瀹炵幇 per-resource 閿併��
+ /// </summary>
+ public sealed class ResourceManager : IResourceManager
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(ResourceManager));
+
+ /// <summary>
+ /// 娲昏穬棰勭害瀛楀吀锛坘ey = ResourceKey.ToString()锛�
+ /// </summary>
+ private readonly ConcurrentDictionary<string, ResourceReservation> _reservations = new();
+
+ /// <summary>
+ /// 姣忚祫婧愪俊鍙烽噺瀛楀吀锛岀敤浜庡疄鐜扮粏绮掑害閿�
+ /// </summary>
+ private readonly ConcurrentDictionary<string, SemaphoreSlim> _resourceLocks = new();
+
+ /// <summary>
+ /// 鍏ㄥ眬涓栦唬鍙凤紝姣忔鏂伴绾﹂�掑
+ /// </summary>
+ private long _generation;
+
+ /// <summary>
+ /// 榛樿绉熺害鏈夋晥鏈燂紙绉掞級
+ /// </summary>
+ private const int DefaultLeaseDurationSeconds = 30;
+
+ /// <inheritdoc/>
+ public async Task<ResourceReservation> ReserveAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ if (owner == null) throw new ArgumentNullException(nameof(owner));
+ if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+ var resourceKey = resource.ToString();
+ var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1));
+
+ try
+ {
+ await semaphore.WaitAsync(ct).ConfigureAwait(false);
+
+ try
+ {
+ // 妫�鏌ユ槸鍚﹀凡鏈� Active 棰勭害
+ if (_reservations.TryGetValue(resourceKey, out var existingReservation))
+ {
+ if (existingReservation.State == ReservationState.Active)
+ {
+ // 妫�鏌ユ槸鍚︿负鍚屼竴 Owner锛堢画鏈熷満鏅級
+ if (existingReservation.Owner.TokenId == owner.TokenId)
+ {
+ // 鍚屼竴鎵�鏈夎�呯画鏈燂細鏇存柊涓栦唬鍙�
+ var renewedGeneration = Interlocked.Increment(ref _generation);
+ owner.Generation = renewedGeneration;
+ existingReservation.Owner.Generation = renewedGeneration;
+
+ Log.Info($"[ResourceManager] 璧勬簮 {resourceKey} 缁湡鎴愬姛锛屾柊涓栦唬鍙�: {renewedGeneration}");
+ return existingReservation;
+ }
+
+ // 涓嶅悓鎵�鏈夎�呭啿绐�
+ var conflictMsg = $"璧勬簮 {resourceKey} 宸茶鎵�鏈夎�� {existingReservation.Owner.OwnerId} 棰勭害锛�" +
+ $"褰撳墠璇锋眰鑰�: {owner.OwnerId}";
+ Log.Warn($"[ResourceManager] 璧勬簮鍐茬獊: {conflictMsg}");
+ throw new ResourceConflictException(conflictMsg);
+ }
+ }
+
+ // 鍒涘缓鏂伴绾�
+ var newGeneration = Interlocked.Increment(ref _generation);
+ owner.Generation = newGeneration;
+
+ var reservation = new ResourceReservation(resource, owner)
+ {
+ State = ReservationState.Active
+ };
+
+ _reservations[resourceKey] = reservation;
+
+ Log.Info($"[ResourceManager] 璧勬簮 {resourceKey} 棰勭害鎴愬姛锛屾墍鏈夎��: {owner.OwnerId}锛屼笘浠e彿: {newGeneration}");
+ return reservation;
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ Log.Warn($"[ResourceManager] 璧勬簮 {resourceKey} 棰勭害琚彇娑�");
+ throw;
+ }
+ catch (ResourceConflictException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[ResourceManager] 璧勬簮 {resourceKey} 棰勭害澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task<IOperationLease> AcquireOperationAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ if (owner == null) throw new ArgumentNullException(nameof(owner));
+ if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+ var resourceKey = resource.ToString();
+
+ try
+ {
+ // 妫�鏌ヨ祫婧愭槸鍚︽湁鏈夋晥棰勭害
+ if (!_reservations.TryGetValue(resourceKey, out var reservation))
+ {
+ throw new InvalidOperationException($"璧勬簮 {resourceKey} 鏈棰勭害锛屾棤娉曡幏鍙栨搷浣滅绾�");
+ }
+
+ if (reservation.State != ReservationState.Active)
+ {
+ throw new InvalidOperationException($"璧勬簮 {resourceKey} 棰勭害鐘舵�佷负 {reservation.State}锛屾棤娉曡幏鍙栨搷浣滅绾�");
+ }
+
+ // 楠岃瘉 Owner 鍖归厤锛圱okenId + Generation锛�
+ if (reservation.Owner.TokenId != owner.TokenId || reservation.Owner.Generation != owner.Generation)
+ {
+ throw new InvalidOperationException(
+ $"璧勬簮 {resourceKey} 鎵�鏈夎�呬笉鍖归厤锛屾湡鏈� TokenId: {reservation.Owner.TokenId}锛孏eneration: {reservation.Owner.Generation}");
+ }
+
+ // 鍒涘缓 OperationLease锛堢煭鏈燂紝榛樿30绉掕繃鏈燂級
+ var lease = new OperationLease(
+ resource,
+ owner,
+ onRelease: () => Log.Debug($"[ResourceManager] 绉熺害閲婃斁: {resourceKey}"),
+ leaseDurationSeconds: DefaultLeaseDurationSeconds);
+
+ Log.Info($"[ResourceManager] 鑾峰彇鎿嶄綔绉熺害: {resourceKey}锛屾墍鏈夎��: {owner.OwnerId}锛屾湁鏁堟湡: {DefaultLeaseDurationSeconds}绉�");
+
+ await Task.CompletedTask; // 淇濇寔寮傛绛惧悕
+ return lease;
+ }
+ catch (InvalidOperationException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[ResourceManager] 鑾峰彇鎿嶄綔绉熺害澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task ReleaseReservationAsync(
+ ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+ {
+ if (owner == null) throw new ArgumentNullException(nameof(owner));
+ if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+ var resourceKey = resource.ToString();
+ var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1));
+
+ try
+ {
+ await semaphore.WaitAsync(ct).ConfigureAwait(false);
+
+ try
+ {
+ if (!_reservations.TryGetValue(resourceKey, out var reservation))
+ {
+ Log.Warn($"[ResourceManager] 灏濊瘯閲婃斁涓嶅瓨鍦ㄧ殑璧勬簮棰勭害: {resourceKey}");
+ return;
+ }
+
+ // 楠岃瘉 Owner 鍖归厤
+ if (reservation.Owner.TokenId != owner.TokenId)
+ {
+ throw new InvalidOperationException(
+ $"璧勬簮 {resourceKey} 閲婃斁澶辫触锛氭墍鏈夎�呬笉鍖归厤锛屾湡鏈� TokenId: {reservation.Owner.TokenId}");
+ }
+
+ // 鏍囪棰勭害涓� Released
+ reservation.State = ReservationState.Released;
+ _reservations.TryRemove(resourceKey, out _);
+
+ Log.Info($"[ResourceManager] 璧勬簮 {resourceKey} 棰勭害宸查噴鏀撅紝鎵�鏈夎��: {owner.OwnerId}");
+ }
+ finally
+ {
+ semaphore.Release();
+ }
+
+ await Task.CompletedTask; // 淇濇寔寮傛绛惧悕
+ }
+ catch (InvalidOperationException)
+ {
+ throw;
+ }
+ catch (OperationCanceledException)
+ {
+ Log.Warn($"[ResourceManager] 璧勬簮 {resourceKey} 閲婃斁鎿嶄綔琚彇娑�");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[ResourceManager] 閲婃斁璧勬簮 {resourceKey} 澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public bool IsReserved(ResourceKey resource, out ResourceReservation? reservation)
+ {
+ if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+ var resourceKey = resource.ToString();
+ var result = _reservations.TryGetValue(resourceKey, out var res);
+
+ if (result && res != null && res.State == ReservationState.Active)
+ {
+ reservation = res;
+ return true;
+ }
+
+ reservation = null;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource)
+ {
+ if (owner == null) throw new ArgumentNullException(nameof(owner));
+ if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+ var resourceKey = resource.ToString();
+
+ if (!_reservations.TryGetValue(resourceKey, out var reservation))
+ return false;
+
+ if (reservation.State != ReservationState.Active)
+ return false;
+
+ return reservation.Owner.TokenId == owner.TokenId &&
+ reservation.Owner.Generation == owner.Generation;
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/CanDriverAdapter.cs b/Aging.Infrastructure/Drivers/CanDriverAdapter.cs
new file mode 100644
index 0000000..1a96682
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/CanDriverAdapter.cs
@@ -0,0 +1,576 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using CanSimulator;
+using log4net;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// CAN 閫氳椹卞姩閫傞厤鍣ㄣ��
+ /// 灏佽 CanSimulator锛屽疄鐜扮粺涓�鐨� IDeviceDriver 鎺ュ彛銆�
+ /// 鍛戒护鏍煎紡绾﹀畾:
+ /// 鍙戦��: "SEND:frameId:data" 濡� "SEND:0x100:01 02 03 04"
+ /// 鏌ヨ:
+ /// "RECV" - 鑾峰彇鎺ユ敹閭涓殑涓嬩竴甯�
+ /// "RECV_ALL" - 鑾峰彇鎺ユ敹閭涓殑鎵�鏈夊抚
+ /// "STATUS" - 鑾峰彇鑺傜偣/鎬荤嚎鐘舵��
+ /// "FILTER:id:mask" - 璁剧疆鎺ユ敹婊ゆ尝鍣�
+ /// "FILTER_CLEAR" - 娓呴櫎鎵�鏈夋护娉㈠櫒
+ /// "PROMISCUOUS" - 璁剧疆娣锋潅妯″紡锛堟帴鏀舵墍鏈夊抚锛�
+ /// "LOG" - 鑾峰彇鎬荤嚎鏃ュ織
+ /// "BUS_STATUS" - 鑾峰彇鎬荤嚎鐘舵�佹憳瑕�
+ /// </summary>
+ public sealed class CanDriverAdapter : IDeviceDriver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(CanDriverAdapter));
+
+ /// <summary>
+ /// 鍏ㄥ眬 CAN 鎬荤嚎妯℃嫙鍣ㄥ疄渚嬶紙鎵�鏈� CAN 椹卞姩鍏变韩锛�
+ /// </summary>
+ private static readonly Dictionary<string, CanBusSimulator> _sharedBuses = new();
+ private static readonly object _busLock = new();
+
+ private readonly string _ip; // 鎬荤嚎鍚嶇О锛堢敤 ip 瀛楁鏄犲皠锛�
+ private readonly int _port; // 鑺傜偣缂栧彿锛堢敤 port 瀛楁鏄犲皠锛�
+ private CanBusSimulator? _bus;
+ private CanNode? _node;
+ private bool _disposed;
+ private readonly Stopwatch _latencyStopwatch = new();
+ private double _lastLatencyMs;
+ private string _lastError = string.Empty;
+ private DateTime? _lastErrorTime;
+ private int _reconnectCount;
+
+ /// <summary>
+ /// 鎺ユ敹鍒扮殑甯х紦瀛橈紙鐢ㄤ簬 RECV 鏌ヨ锛�
+ /// </summary>
+ private readonly Queue<CanFrame> _recvBuffer = new();
+
+ /// <inheritdoc/>
+ public string DeviceId { get; }
+
+ /// <inheritdoc/>
+ public bool IsConnected => _node != null && _node.IsActive;
+
+ /// <inheritdoc/>
+ public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+
+ /// <summary>
+ /// 鍒濆鍖� CAN 椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="busName">鎬荤嚎鍚嶇О锛堝 CAN_BUS_1锛�</param>
+ /// <param name="nodeIndex">鑺傜偣缂栧彿锛堢敤浜庡尯鍒嗗悓涓�鎬荤嚎涓婄殑涓嶅悓璁惧锛�</param>
+ public CanDriverAdapter(string deviceId, string busName, int nodeIndex)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ _ip = busName ?? "CAN_BUS_1";
+ _port = nodeIndex;
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎴栧垱寤哄叡浜�荤嚎
+ /// </summary>
+ private CanBusSimulator GetOrCreateBus(string busName)
+ {
+ lock (_busLock)
+ {
+ if (!_sharedBuses.TryGetValue(busName, out var bus))
+ {
+ bus = new CanBusSimulator(busName);
+ _sharedBuses[busName] = bus;
+ Log.Info($"[CAN] 鍒涘缓鏂扮殑铏氭嫙鎬荤嚎: {busName}");
+ }
+ return bus;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task ConnectAsync(CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ Log.Info($"[CAN] 璁惧 {DeviceId} 姝e湪杩炴帴鍒版�荤嚎 {_ip} (鑺傜偣: {_port})...");
+
+ try
+ {
+ // 鑾峰彇鎴栧垱寤哄叡浜�荤嚎
+ _bus = GetOrCreateBus(_ip);
+
+ // 濡傛灉鎬荤嚎鏈惎鍔紝鑷姩鍚姩
+ if (_bus.State != CanBusState.Running)
+ {
+ _bus.Start();
+ }
+
+ // 娉ㄥ唽鑺傜偣
+ string nodeId = $"{DeviceId}_node{_port}";
+ _node = _bus.RegisterNode(nodeId, DeviceId);
+
+ // 璁剧疆榛樿婊ゆ尝鍣紙鎺ユ敹鎵�鏈夊抚锛�
+ _node.SetPromiscuousMode();
+
+ // 婵�娲昏妭鐐�
+ _node.Activate();
+
+ // 璁㈤槄鎺ユ敹浜嬩欢
+ _node.FrameReceived += OnFrameReceived;
+
+ _lastError = string.Empty;
+ Log.Info($"[CAN] 璁惧 {DeviceId} 宸叉垚鍔熻繛鎺ュ埌鎬荤嚎 {_ip} (鑺傜偣: {_node.NodeId})");
+ OnConnectionChanged(true, "杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[CAN] 璁惧 {DeviceId} 杩炴帴寮傚父: {ex.Message}", ex);
+ _node?.Deactivate();
+ _node = null;
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task DisconnectAsync()
+ {
+ ThrowIfDisposed();
+ Log.Info($"[CAN] 璁惧 {DeviceId} 姝e湪鏂紑杩炴帴...");
+
+ try
+ {
+ if (_node != null)
+ {
+ _node.FrameReceived -= OnFrameReceived;
+ _node.Deactivate();
+ _bus?.UnregisterNode(_node.NodeId);
+ _node = null;
+ }
+
+ Log.Info($"[CAN] 璁惧 {DeviceId} 宸叉柇寮�杩炴帴");
+ OnConnectionChanged(false, "涓诲姩鏂紑");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[CAN] 璁惧 {DeviceId} 鏂紑杩炴帴寮傚父: {ex.Message}", ex);
+ _node = null;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task<string> SendCommandAsync(string command, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[CAN] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[CAN] 璁惧 {DeviceId} 鍙戦�佸懡浠�: {command}");
+
+ // 瑙f瀽鍛戒护鏍煎紡: "SEND:frameId:data"
+ string[] parts = command.Split(':');
+ if (parts.Length < 3)
+ {
+ throw new ArgumentException($"[CAN] 鍛戒护鏍煎紡閿欒锛屽簲涓� SEND:frameId:data锛屽疄闄�: {command}");
+ }
+
+ string operation = parts[0].ToUpper();
+ if (operation != "SEND")
+ {
+ throw new ArgumentException($"[CAN] 涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {operation}锛屽簲涓� SEND");
+ }
+
+ // 瑙f瀽甯� ID
+ string frameIdStr = parts[1];
+ uint frameId = CanFrame.ParseFrameId(frameIdStr);
+
+ // 瑙f瀽鏁版嵁
+ string dataStr = parts[2];
+ byte[] data = CanFrame.ParseData(dataStr);
+
+ // 楠岃瘉鏁版嵁闀垮害
+ if (data.Length > 8)
+ {
+ throw new ArgumentException($"[CAN] 鏁版嵁闀垮害瓒呴檺: {data.Length} 瀛楄妭锛堟渶澶� 8 瀛楄妭锛�");
+ }
+
+ // 鍒涘缓骞跺彂閫佸抚
+ var frame = new CanFrame
+ {
+ FrameId = frameId,
+ Format = frameId > 0x7FF ? CanFrameFormat.Extended : CanFrameFormat.Standard,
+ Type = CanFrameType.Data,
+ Data = data
+ };
+
+ _node!.SendFrame(frame);
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+
+ string response = $"OK:TX:{frame.FrameIdHex}:{frame.Dlc}";
+ Log.Debug($"[CAN] 璁惧 {DeviceId} 鍙戦�佹垚鍔� (寤惰繜 {_lastLatencyMs:F2}ms): {response}");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[CAN] 璁惧 {DeviceId} 鍙戦�佸懡浠ゅけ璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task<string> QueryAsync(string query, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[CAN] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[CAN] 璁惧 {DeviceId} 鏌ヨ: {query}");
+
+ string[] parts = query.Split(':');
+ string operation = parts[0].ToUpper();
+
+ string response;
+ switch (operation)
+ {
+ case "RECV":
+ response = HandleRecv();
+ break;
+
+ case "RECV_ALL":
+ response = HandleRecvAll();
+ break;
+
+ case "STATUS":
+ response = HandleStatus();
+ break;
+
+ case "FILTER":
+ response = HandleFilter(parts);
+ break;
+
+ case "FILTER_CLEAR":
+ _node!.ClearFilters();
+ response = "OK:婊ゆ尝鍣ㄥ凡娓呴櫎";
+ break;
+
+ case "PROMISCUOUS":
+ _node!.SetPromiscuousMode();
+ response = "OK:娣锋潅妯″紡宸插惎鐢�";
+ break;
+
+ case "AUTO_RESPONSE":
+ response = HandleAutoResponse(parts);
+ break;
+
+ case "RESPONSE_DELAY":
+ response = HandleResponseDelay(parts);
+ break;
+
+ case "LOG":
+ response = HandleLog();
+ break;
+
+ case "BUS_STATUS":
+ response = _bus?.GetStatusSummary() ?? "鎬荤嚎鏈垵濮嬪寲";
+ break;
+
+ default:
+ throw new ArgumentException($"[CAN] 涓嶆敮鎸佺殑鏌ヨ绫诲瀷: {operation}");
+ }
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ Log.Debug($"[CAN] 璁惧 {DeviceId} 鏌ヨ鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[CAN] 璁惧 {DeviceId} 鏌ヨ澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 澶勭悊 RECV 鏌ヨ - 鑾峰彇涓嬩竴甯�
+ /// </summary>
+ private string HandleRecv()
+ {
+ // 鍏堜粠缂撳啿鍖哄彇
+ if (_recvBuffer.Count > 0)
+ {
+ var frame = _recvBuffer.Dequeue();
+ return $"RECV:{frame.FrameIdHex}:{frame.DataHex}:{frame.Timestamp:HH:mm:ss.fff}";
+ }
+
+ // 浠庤妭鐐归偖绠卞彇
+ if (_node!.TryDequeueReceived(out var frame2))
+ {
+ return $"RECV:{frame2!.FrameIdHex}:{frame2.DataHex}:{frame2.Timestamp:HH:mm:ss.fff}";
+ }
+
+ return "RECV:EMPTY";
+ }
+
+ /// <summary>
+ /// 澶勭悊 RECV_ALL 鏌ヨ - 鑾峰彇鎵�鏈夊抚
+ /// </summary>
+ private string HandleRecvAll()
+ {
+ var frames = new List<string>();
+
+ // 鍏堝彇缂撳啿鍖�
+ while (_recvBuffer.Count > 0)
+ {
+ var f = _recvBuffer.Dequeue();
+ frames.Add($"{f.FrameIdHex}:{f.DataHex}");
+ }
+
+ // 鍐嶅彇鑺傜偣閭
+ var mailbox = _node!.PeekReceiveMailbox();
+ _node.ClearReceiveMailbox();
+ foreach (var f in mailbox)
+ {
+ frames.Add($"{f.FrameIdHex}:{f.DataHex}:{f.Timestamp:HH:mm:ss.fff}");
+ }
+
+ return frames.Count > 0 ? $"RECV_ALL:{frames.Count}:{string.Join("|", frames)}" : "RECV_ALL:0";
+ }
+
+ /// <summary>
+ /// 澶勭悊 STATUS 鏌ヨ
+ /// </summary>
+ private string HandleStatus()
+ {
+ var nodeStatus = _node?.GetStatusSummary() ?? "鑺傜偣鏈垵濮嬪寲";
+ var busStatus = _bus?.GetStatusSummary() ?? "鎬荤嚎鏈垵濮嬪寲";
+ return $"STATUS:{nodeStatus}|{busStatus}";
+ }
+
+ /// <summary>
+ /// 澶勭悊 FILTER 璁剧疆
+ /// </summary>
+ private string HandleFilter(string[] parts)
+ {
+ if (parts.Length < 3)
+ {
+ throw new ArgumentException("[CAN] FILTER 鍛戒护鏍煎紡閿欒锛屽簲涓� FILTER:id:mask");
+ }
+
+ uint filterId = CanFrame.ParseFrameId(parts[1]);
+ uint mask = CanFrame.ParseFrameId(parts[2]);
+
+ _node!.AddFilter(new AcceptanceFilter
+ {
+ FilterId = filterId,
+ Mask = mask,
+ IsExtended = filterId > 0x7FF
+ });
+
+ return $"OK:婊ゆ尝鍣ㄥ凡娣诲姞 ID=0x{filterId:X3}, Mask=0x{mask:X3}";
+ }
+
+ /// <summary>
+ /// 澶勭悊 AUTO_RESPONSE 鍛戒护
+ /// 鏍煎紡: AUTO_RESPONSE:on|off
+ /// </summary>
+ private string HandleAutoResponse(string[] parts)
+ {
+ if (parts.Length < 2)
+ {
+ // 鏌ヨ褰撳墠鐘舵��
+ bool current = _node!.AutoResponseEnabled;
+ return $"AUTO_RESPONSE:{(current ? "ON" : "OFF")}";
+ }
+
+ string mode = parts[1].Trim().ToUpper();
+ switch (mode)
+ {
+ case "ON":
+ case "1":
+ case "TRUE":
+ case "ENABLE":
+ _node!.AutoResponseEnabled = true;
+ Log.Info($"[CAN] 璁惧 {DeviceId} 鍚敤鑷姩鍝嶅簲妯″紡");
+ return "OK:鑷姩鍝嶅簲妯″紡宸插惎鐢�";
+
+ case "OFF":
+ case "0":
+ case "FALSE":
+ case "DISABLE":
+ _node!.AutoResponseEnabled = false;
+ Log.Info($"[CAN] 璁惧 {DeviceId} 绂佺敤鑷姩鍝嶅簲妯″紡");
+ return "OK:鑷姩鍝嶅簲妯″紡宸茬鐢�";
+
+ default:
+ throw new ArgumentException($"[CAN] AUTO_RESPONSE 鍙傛暟閿欒: {mode}锛屽簲涓� on/off");
+ }
+ }
+
+ /// <summary>
+ /// 澶勭悊 RESPONSE_DELAY 鍛戒护
+ /// 鏍煎紡: RESPONSE_DELAY:姣鏁�
+ /// </summary>
+ private string HandleResponseDelay(string[] parts)
+ {
+ if (parts.Length < 2)
+ {
+ // 鏌ヨ褰撳墠寤惰繜
+ return $"RESPONSE_DELAY:{_node!.AutoResponseDelayMs}ms";
+ }
+
+ if (!int.TryParse(parts[1].Trim(), out int delayMs) || delayMs < 0)
+ {
+ throw new ArgumentException($"[CAN] RESPONSE_DELAY 鍙傛暟閿欒: {parts[1]}锛屽簲涓洪潪璐熸暣鏁帮紙姣锛�");
+ }
+
+ _node!.AutoResponseDelayMs = delayMs;
+ Log.Info($"[CAN] 璁惧 {DeviceId} 璁剧疆鍝嶅簲寤惰繜: {delayMs}ms");
+ return $"OK:鍝嶅簲寤惰繜宸茶缃负 {delayMs}ms";
+ }
+
+ /// <summary>
+ /// 澶勭悊 LOG 鏌ヨ
+ /// </summary>
+ private string HandleLog()
+ {
+ var log = _bus?.GetBusLog(50) ?? new List<CanFrame>();
+ if (log.Count == 0) return "LOG:EMPTY";
+
+ var entries = log.Select(f =>
+ $"[{f.Timestamp:HH:mm:ss.fff}]{f.SourceNodeId}>{f.FrameIdHex}:{f.DataHex}");
+ return $"LOG:{log.Count}:{string.Join("|", entries)}";
+ }
+
+ /// <summary>
+ /// 甯ф帴鏀朵簨浠讹紙鍏紑缁欏閮ㄨ闃咃級
+ /// </summary>
+ public event EventHandler<CanFrame>? DriverFrameReceived;
+
+ /// <summary>
+ /// 甯ф帴鏀朵簨浠跺鐞�
+ /// </summary>
+ private void OnFrameReceived(object? sender, CanFrame frame)
+ {
+ // 鏀惧叆鎺ユ敹缂撳啿鍖猴紙鐢ㄤ簬 RECV/RECV_ALL 鏌ヨ锛�
+ while (_recvBuffer.Count >= 1000)
+ {
+ _recvBuffer.Dequeue();
+ }
+ _recvBuffer.Enqueue(frame);
+
+ // 瑙﹀彂鍏紑浜嬩欢锛堢敤浜庡嵆鏃舵樉绀猴級
+ DriverFrameReceived?.Invoke(this, frame);
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus()
+ {
+ return new DeviceDriverStatus
+ {
+ IsConnected = IsConnected,
+ IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
+ LastError = _lastError,
+ LastErrorTime = _lastErrorTime,
+ LatencyMs = _lastLatencyMs,
+ ReconnectCount = _reconnectCount
+ };
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ if (_node != null)
+ {
+ _node.FrameReceived -= OnFrameReceived;
+ _node.Deactivate();
+ _bus?.UnregisterNode(_node.NodeId);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[CAN] 璁惧 {DeviceId} 閲婃斁璧勬簮寮傚父: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂鎵�鏈夋�荤嚎鍜岃妭鐐癸紙鐢ㄤ簬娴嬭瘯娓呯悊锛�
+ /// </summary>
+ public static void ResetAllBuses()
+ {
+ lock (_busLock)
+ {
+ foreach (var bus in _sharedBuses.Values)
+ {
+ bus.Dispose();
+ }
+ _sharedBuses.Clear();
+ Log.Info("[CAN] 鎵�鏈夎櫄鎷熸�荤嚎宸查噸缃�");
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂鎵�鏈夋�荤嚎瀹炰緥锛堜緵澶栭儴鏌ヨ锛�
+ /// </summary>
+ public static IReadOnlyDictionary<string, CanBusSimulator> GetAllBuses()
+ {
+ lock (_busLock)
+ {
+ return new Dictionary<string, CanBusSimulator>(_sharedBuses);
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ private void OnConnectionChanged(bool isConnected, string reason)
+ {
+ ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
+ {
+ DeviceId = DeviceId,
+ IsConnected = isConnected,
+ Reason = reason
+ });
+
+ if (!isConnected)
+ {
+ _reconnectCount++;
+ }
+ }
+
+ /// <summary>
+ /// 妫�鏌ユ槸鍚﹀凡閲婃斁
+ /// </summary>
+ private void ThrowIfDisposed()
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(CanDriverAdapter));
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/DriverFactory.cs b/Aging.Infrastructure/Drivers/DriverFactory.cs
new file mode 100644
index 0000000..dd852ce
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/DriverFactory.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using Aging.Infrastructure.Contracts;
+using log4net;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// 璁惧椹卞姩宸ュ巶銆�
+ /// 鏍规嵁浼犺緭鍗忚绫诲瀷鍜岀鐐逛俊鎭垱寤哄搴旂殑椹卞姩閫傞厤鍣ㄥ疄渚嬨��
+ /// </summary>
+ public static class DriverFactory
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(DriverFactory));
+
+ /// <summary>
+ /// 鏍规嵁浼犺緭鍗忚绫诲瀷鍜岀鐐瑰垱寤洪┍鍔ㄩ�傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="transport">浼犺緭鍗忚绫诲瀷: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param>
+ /// <param name="endpoint">绔偣淇℃伅锛屾牸寮�: "ip:port" 鎴� "ip:port:extra"</param>
+ /// <returns>IDeviceDriver 瀹炰緥</returns>
+ /// <exception cref="NotSupportedException">涓嶆敮鎸佺殑浼犺緭鍗忚绫诲瀷</exception>
+ /// <exception cref="ArgumentException">绔偣鏍煎紡閿欒</exception>
+ public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint)
+ {
+ return CreateDriver(deviceId, transport, endpoint, null);
+ }
+
+ /// <summary>
+ /// 鏍规嵁浼犺緭鍗忚绫诲瀷銆佺鐐瑰拰棰濆鍙傛暟鍒涘缓椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="transport">浼犺緭鍗忚绫诲瀷: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param>
+ /// <param name="endpoint">绔偣淇℃伅锛屾牸寮�: "ip:port" 鎴� "ip:port:extra"</param>
+ /// <param name="extraParams">棰濆鍙傛暟锛堝彲閫夛級</param>
+ /// <returns>IDeviceDriver 瀹炰緥</returns>
+ /// <exception cref="NotSupportedException">涓嶆敮鎸佺殑浼犺緭鍗忚绫诲瀷</exception>
+ /// <exception cref="ArgumentException">绔偣鏍煎紡閿欒</exception>
+ public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint, Dictionary<string, string>? extraParams)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentNullException(nameof(deviceId));
+ if (string.IsNullOrWhiteSpace(transport))
+ throw new ArgumentNullException(nameof(transport));
+ if (string.IsNullOrWhiteSpace(endpoint))
+ throw new ArgumentNullException(nameof(endpoint));
+
+ Log.Info($"[DriverFactory] 鍒涘缓璁惧椹卞姩: DeviceId={deviceId}, Transport={transport}, Endpoint={endpoint}");
+
+ string[] parts = endpoint.Split(':');
+ if (parts.Length < 2)
+ {
+ throw new ArgumentException($"绔偣鏍煎紡閿欒锛屽簲涓� 'ip:port' 鎴� 'ip:port:extra'锛屽疄闄�: {endpoint}");
+ }
+
+ string ip = parts[0];
+ if (!int.TryParse(parts[1], out int port))
+ {
+ throw new ArgumentException($"绔彛鍙锋牸寮忛敊璇�: {parts[1]}");
+ }
+
+ switch (transport.ToUpper())
+ {
+ case "TCP":
+ Log.Info($"[DriverFactory] 鍒涘缓 TCP 鍘熷閫氫俊椹卞姩: {ip}:{port}");
+ return new TcpRawDriverAdapter(deviceId, ip, port);
+
+ case "SCPI":
+ Log.Info($"[DriverFactory] 鍒涘缓 SCPI 鍗忚椹卞姩: {ip}:{port}");
+ return new ScpiDriverAdapter(deviceId, ip, port);
+
+ case "MODBUS":
+ byte slaveId = 1;
+ if (extraParams != null && extraParams.TryGetValue("slaveId", out string slaveIdStr))
+ {
+ if (!byte.TryParse(slaveIdStr, out slaveId))
+ {
+ slaveId = 1;
+ }
+ }
+ // 濡傛灉 endpoint 鏈夌涓夐儴鍒嗭紝浣滀负 slaveId
+ else if (parts.Length >= 3 && byte.TryParse(parts[2], out byte parsedSlaveId))
+ {
+ slaveId = parsedSlaveId;
+ }
+ Log.Info($"[DriverFactory] 鍒涘缓 ModBus TCP 椹卞姩: {ip}:{port} (浠庣珯: {slaveId})");
+ return new ModBusDriverAdapter(deviceId, ip, port, slaveId);
+
+ case "S7":
+ Log.Info($"[DriverFactory] 鍒涘缓 S7 PLC 椹卞姩: {ip}:{port}");
+ return new S7DriverAdapter(deviceId, ip, port);
+
+ case "SERIAL":
+ // TODO: 瀹炵幇涓插彛椹卞姩閫傞厤鍣�
+ throw new NotSupportedException("涓插彛椹卞姩閫傞厤鍣ㄥ皻鏈疄鐜�");
+
+ case "CAN":
+ // CAN 椹卞姩: endpoint 鏍煎紡涓� "busName:nodeIndex" 濡� "CAN_BUS_1:0"
+ string busName = ip; // ip 瀛楁鐢ㄤ綔鎬荤嚎鍚嶇О
+ int nodeIndex = port; // port 瀛楁鐢ㄤ綔鑺傜偣缂栧彿
+ Log.Info($"[DriverFactory] 鍒涘缓 CAN 铏氭嫙椹卞姩: Bus={busName}, Node={nodeIndex}");
+ return new CanDriverAdapter(deviceId, busName, nodeIndex);
+
+ default:
+ throw new NotSupportedException($"涓嶆敮鎸佺殑浼犺緭鍗忚绫诲瀷: {transport}銆傛敮鎸佺殑绫诲瀷: Tcp, SCPI, ModBus, S7, Serial, CAN");
+ }
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs b/Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs
new file mode 100644
index 0000000..784e64b
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/ModBusDriverAdapter.cs
@@ -0,0 +1,347 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using HslCommunication;
+using HslCommunication.ModBus;
+using log4net;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// ModBus TCP 椹卞姩閫傞厤鍣ㄣ��
+ /// 灏佽 HslCommunication.ModBusTcpNet锛屽疄鐜扮粺涓�鐨� IDeviceDriver 鎺ュ彛銆�
+ /// 鍛戒护鏍煎紡绾﹀畾:
+ /// 鍐欏叆: "WRITE:address:value" 濡� "WRITE:40001:100" 鎴� "WRITE:00001:TRUE"
+ /// 璇诲彇: "READ:address" 濡� "READ:00001" 鎴� "READ:40001"
+ /// </summary>
+ public sealed class ModBusDriverAdapter : IDeviceDriver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(ModBusDriverAdapter));
+
+ private readonly string _ip;
+ private readonly int _port;
+ private readonly byte _slaveId;
+ private ModbusTcpNet? _client;
+ private bool _disposed;
+ private readonly Stopwatch _latencyStopwatch = new Stopwatch();
+ private double _lastLatencyMs;
+ private string _lastError = string.Empty;
+ private DateTime? _lastErrorTime;
+ private int _reconnectCount;
+
+ /// <inheritdoc/>
+ public string DeviceId { get; }
+
+ /// <inheritdoc/>
+ public bool IsConnected => _client != null;
+
+ /// <inheritdoc/>
+ public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+
+ /// <summary>
+ /// 鍒濆鍖� ModBus TCP 椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="ip">鐩爣 IP 鍦板潃</param>
+ /// <param name="port">鐩爣绔彛鍙�</param>
+ /// <param name="slaveId">浠庣珯鍦板潃锛堥粯璁や负 1锛�</param>
+ public ModBusDriverAdapter(string deviceId, string ip, int port, byte slaveId = 1)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ _ip = ip ?? throw new ArgumentNullException(nameof(ip));
+ _port = port;
+ _slaveId = slaveId;
+ }
+
+ /// <inheritdoc/>
+ public Task ConnectAsync(CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ Log.Info($"[ModBus] 璁惧 {DeviceId} 姝e湪杩炴帴 {_ip}:{_port} (浠庣珯: {_slaveId})...");
+
+ try
+ {
+ _client = new ModbusTcpNet(_ip, _port, _slaveId);
+ OperateResult result = _client.ConnectServer();
+
+ if (!result.IsSuccess)
+ {
+ _lastError = result.Message;
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[ModBus] 璁惧 {DeviceId} 杩炴帴澶辫触: {result.Message}");
+ }
+
+ _lastError = string.Empty;
+ Log.Info($"[ModBus] 璁惧 {DeviceId} 宸叉垚鍔熻繛鎺ュ埌 {_ip}:{_port}");
+ OnConnectionChanged(true, "杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[ModBus] 璁惧 {DeviceId} 杩炴帴寮傚父: {ex.Message}", ex);
+ _client?.ConnectClose();
+ _client?.Dispose();
+ _client = null;
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task DisconnectAsync()
+ {
+ ThrowIfDisposed();
+ Log.Info($"[ModBus] 璁惧 {DeviceId} 姝e湪鏂紑杩炴帴...");
+
+ try
+ {
+ if (_client != null)
+ {
+ _client.ConnectClose();
+ _client.Dispose();
+ _client = null;
+ }
+
+ Log.Info($"[ModBus] 璁惧 {DeviceId} 宸叉柇寮�杩炴帴");
+ OnConnectionChanged(false, "涓诲姩鏂紑");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[ModBus] 璁惧 {DeviceId} 鏂紑杩炴帴寮傚父: {ex.Message}", ex);
+ _client = null;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task<string> SendCommandAsync(string command, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[ModBus] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[ModBus] 璁惧 {DeviceId} 鍙戦�佸懡浠�: {command}");
+
+ // 瑙f瀽鍛戒护鏍煎紡: "WRITE:address:value"
+ string[] parts = command.Split(':');
+ if (parts.Length < 3)
+ {
+ throw new ArgumentException($"[ModBus] 鍛戒护鏍煎紡閿欒锛屽簲涓� WRITE:address:value锛屽疄闄�: {command}");
+ }
+
+ string operation = parts[0].ToUpper();
+ string address = parts[1];
+ string value = parts[2];
+
+ if (operation != "WRITE")
+ {
+ throw new ArgumentException($"[ModBus] 涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {operation}锛屽簲涓� WRITE");
+ }
+
+ // 鏍规嵁鍊煎垽鏂暟鎹被鍨嬪苟鍐欏叆
+ OperateResult result;
+ if (bool.TryParse(value, out bool boolVal))
+ {
+ result = _client.Write(address, boolVal);
+ }
+ else if (short.TryParse(value, out short shortVal))
+ {
+ result = _client.Write(address, shortVal);
+ }
+ else if (int.TryParse(value, out int intVal))
+ {
+ result = _client.Write(address, intVal);
+ }
+ else if (float.TryParse(value, out float floatVal))
+ {
+ result = _client.Write(address, floatVal);
+ }
+ else
+ {
+ throw new ArgumentException($"[ModBus] 鏃犳硶瑙f瀽鐨勫��: {value}");
+ }
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+
+ if (!result.IsSuccess)
+ {
+ _lastError = result.Message;
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[ModBus] 鍐欏叆澶辫触: {result.Message}");
+ }
+
+ string response = "OK";
+ Log.Debug($"[ModBus] 璁惧 {DeviceId} 鍐欏叆鎴愬姛 (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[ModBus] 璁惧 {DeviceId} 鍙戦�佸懡浠ゅけ璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task<string> QueryAsync(string query, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[ModBus] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[ModBus] 璁惧 {DeviceId} 鍙戦�佹煡璇�: {query}");
+
+ // 瑙f瀽鏌ヨ鏍煎紡: "READ:address"
+ string[] parts = query.Split(':');
+ if (parts.Length < 2)
+ {
+ throw new ArgumentException($"[ModBus] 鏌ヨ鏍煎紡閿欒锛屽簲涓� READ:address锛屽疄闄�: {query}");
+ }
+
+ string operation = parts[0].ToUpper();
+ string address = parts[1];
+
+ if (operation != "READ")
+ {
+ throw new ArgumentException($"[ModBus] 涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {operation}锛屽簲涓� READ");
+ }
+
+ // 灏濊瘯璇诲彇涓嶅悓绫诲瀷锛堟牴鎹湴鍧�鑼冨洿鑷姩鍒ゆ柇锛�
+ // 00001-09999: 绾垮湀锛圔OOL锛�
+ // 10001-19999: 绂绘暎杈撳叆锛圔OOL锛�
+ // 30001-39999: 杈撳叆瀵勫瓨鍣紙INT16锛�
+ // 40001-49999: 淇濇寔瀵勫瓨鍣紙INT16锛�
+ string response;
+ int addrNum;
+ if (int.TryParse(address, out addrNum))
+ {
+ if (addrNum >= 1 && addrNum <= 9999)
+ {
+ // 绾垮湀璇诲彇
+ var result = _client.ReadBool(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+ else if (addrNum >= 10001 && addrNum <= 19999)
+ {
+ // 绂绘暎杈撳叆璇诲彇
+ var result = _client.ReadBool(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+ else if (addrNum >= 30001 && addrNum <= 39999)
+ {
+ // 杈撳叆瀵勫瓨鍣ㄨ鍙�
+ var result = _client.ReadInt16(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+ else if (addrNum >= 40001 && addrNum <= 49999)
+ {
+ // 淇濇寔瀵勫瓨鍣ㄨ鍙�
+ var result = _client.ReadInt16(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+ else
+ {
+ // 榛樿璇诲彇 INT16
+ var result = _client.ReadInt16(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+ }
+ else
+ {
+ // 闈炴暟瀛楀湴鍧�锛屽皾璇曡鍙� INT16
+ var result = _client.ReadInt16(address);
+ response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}";
+ }
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ Log.Debug($"[ModBus] 璁惧 {DeviceId} 鏌ヨ鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[ModBus] 璁惧 {DeviceId} 鏌ヨ澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus()
+ {
+ return new DeviceDriverStatus
+ {
+ IsConnected = IsConnected,
+ IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
+ LastError = _lastError,
+ LastErrorTime = _lastErrorTime,
+ LatencyMs = _lastLatencyMs,
+ ReconnectCount = _reconnectCount
+ };
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ if (_client != null)
+ {
+ _client.ConnectClose();
+ _client.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[ModBus] 璁惧 {DeviceId} 閲婃斁璧勬簮寮傚父: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ private void OnConnectionChanged(bool isConnected, string reason)
+ {
+ ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
+ {
+ DeviceId = DeviceId,
+ IsConnected = isConnected,
+ Reason = reason
+ });
+
+ if (!isConnected)
+ {
+ _reconnectCount++;
+ }
+ }
+
+ /// <summary>
+ /// 妫�鏌ユ槸鍚﹀凡閲婃斁
+ /// </summary>
+ private void ThrowIfDisposed()
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(ModBusDriverAdapter));
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/S7DriverAdapter.cs b/Aging.Infrastructure/Drivers/S7DriverAdapter.cs
new file mode 100644
index 0000000..b635e2d
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/S7DriverAdapter.cs
@@ -0,0 +1,407 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using log4net;
+using S7Driver;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// 瑗块棬瀛� S7 PLC 椹卞姩閫傞厤鍣ㄣ��
+ /// 灏佽 S7Driver.PLCS7Driver锛屽疄鐜扮粺涓�鐨� IDeviceDriver 鎺ュ彛銆�
+ /// 鍛戒护鏍煎紡绾﹀畾:
+ /// 鍐欏叆: "WRITE:DBx.DBXy.z:value" 濡� "WRITE:DB1.DBX0.0:TRUE"
+ /// 璇诲彇: "READ:DBx.DBWy" 濡� "READ:DB1.DBD0"
+ /// 鏀寔鏁版嵁绫诲瀷: BOOL, INT, REAL
+ /// </summary>
+ public sealed class S7DriverAdapter : IDeviceDriver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(S7DriverAdapter));
+
+ private readonly string _ip;
+ private readonly int _port;
+ private PLCS7Driver? _driver;
+ private bool _disposed;
+ private readonly Stopwatch _latencyStopwatch = new Stopwatch();
+ private double _lastLatencyMs;
+ private string _lastError = string.Empty;
+ private DateTime? _lastErrorTime;
+ private int _reconnectCount;
+
+ /// <inheritdoc/>
+ public string DeviceId { get; }
+
+ /// <inheritdoc/>
+ public bool IsConnected => _driver != null;
+
+ /// <inheritdoc/>
+ public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+
+ /// <summary>
+ /// 鍒濆鍖� S7 PLC 椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="ip">PLC IP 鍦板潃</param>
+ /// <param name="port">PLC 绔彛鍙凤紙閫氬父涓� 102锛�</param>
+ public S7DriverAdapter(string deviceId, string ip, int port)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ _ip = ip ?? throw new ArgumentNullException(nameof(ip));
+ _port = port;
+ }
+
+ /// <inheritdoc/>
+ public Task ConnectAsync(CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ Log.Info($"[S7] 璁惧 {DeviceId} 姝e湪杩炴帴 {_ip}:{_port}...");
+
+ try
+ {
+ _driver = new PLCS7Driver(_ip, _port);
+ bool success = _driver.Connect(out string errMsg);
+
+ if (!success)
+ {
+ _lastError = errMsg;
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[S7] 璁惧 {DeviceId} 杩炴帴澶辫触: {errMsg}");
+ }
+
+ _lastError = string.Empty;
+ Log.Info($"[S7] 璁惧 {DeviceId} 宸叉垚鍔熻繛鎺ュ埌 {_ip}:{_port}");
+ OnConnectionChanged(true, "杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[S7] 璁惧 {DeviceId} 杩炴帴寮傚父: {ex.Message}", ex);
+ _driver?.Dispose();
+ _driver = null;
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task DisconnectAsync()
+ {
+ ThrowIfDisposed();
+ Log.Info($"[S7] 璁惧 {DeviceId} 姝e湪鏂紑杩炴帴...");
+
+ try
+ {
+ if (_driver != null)
+ {
+ _driver.DisConnect(out string errMsg);
+ _driver.Dispose();
+ _driver = null;
+ }
+
+ Log.Info($"[S7] 璁惧 {DeviceId} 宸叉柇寮�杩炴帴");
+ OnConnectionChanged(false, "涓诲姩鏂紑");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[S7] 璁惧 {DeviceId} 鏂紑杩炴帴寮傚父: {ex.Message}", ex);
+ _driver?.Dispose();
+ _driver = null;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task<string> SendCommandAsync(string command, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[S7] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[S7] 璁惧 {DeviceId} 鍙戦�佸懡浠�: {command}");
+
+ // 瑙f瀽鍛戒护鏍煎紡: "WRITE:DBx.DBXy.z:value" 鎴� "WRITE:DBx.DBWy:value"
+ string[] parts = command.Split(':');
+ if (parts.Length < 3)
+ {
+ throw new ArgumentException($"[S7] 鍛戒护鏍煎紡閿欒锛屽簲涓� WRITE:address:value锛屽疄闄�: {command}");
+ }
+
+ string operation = parts[0].ToUpper();
+ string address = parts[1];
+ string value = parts[2];
+
+ if (operation != "WRITE")
+ {
+ throw new ArgumentException($"[S7] 涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {operation}锛屽簲涓� WRITE");
+ }
+
+ // 瑙f瀽鍦板潃鍜屽�硷紝鎵ц鍐欏叆
+ bool success = ParseAndWrite(address, value, out string msg);
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+
+ if (!success)
+ {
+ _lastError = msg;
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[S7] 鍐欏叆澶辫触: {msg}");
+ }
+
+ string response = "OK";
+ Log.Debug($"[S7] 璁惧 {DeviceId} 鍐欏叆鎴愬姛 (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[S7] 璁惧 {DeviceId} 鍙戦�佸懡浠ゅけ璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task<string> QueryAsync(string query, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[S7] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[S7] 璁惧 {DeviceId} 鍙戦�佹煡璇�: {query}");
+
+ // 瑙f瀽鏌ヨ鏍煎紡: "READ:DBx.DBWy" 鎴� "READ:DBx.DBXy.z"
+ string[] parts = query.Split(':');
+ if (parts.Length < 2)
+ {
+ throw new ArgumentException($"[S7] 鏌ヨ鏍煎紡閿欒锛屽簲涓� READ:address锛屽疄闄�: {query}");
+ }
+
+ string operation = parts[0].ToUpper();
+ string address = parts[1];
+
+ if (operation != "READ")
+ {
+ throw new ArgumentException($"[S7] 涓嶆敮鎸佺殑鎿嶄綔绫诲瀷: {operation}锛屽簲涓� READ");
+ }
+
+ // 瑙f瀽鍦板潃骞舵墽琛岃鍙�
+ string response = ParseAndRead(address);
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ Log.Debug($"[S7] 璁惧 {DeviceId} 鏌ヨ鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[S7] 璁惧 {DeviceId} 鏌ヨ澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus()
+ {
+ return new DeviceDriverStatus
+ {
+ IsConnected = IsConnected,
+ IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
+ LastError = _lastError,
+ LastErrorTime = _lastErrorTime,
+ LatencyMs = _lastLatencyMs,
+ ReconnectCount = _reconnectCount
+ };
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ _driver?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[S7] 璁惧 {DeviceId} 閲婃斁璧勬簮寮傚父: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 瑙f瀽鍦板潃骞舵墽琛屽啓鍏ユ搷浣�
+ /// </summary>
+ /// <param name="address">S7 鍦板潃锛屽 "DB1.DBX0.0" 鎴� "DB1.DBW10"</param>
+ /// <param name="value">瑕佸啓鍏ョ殑鍊�</param>
+ /// <param name="msg">閿欒淇℃伅</param>
+ /// <returns>鏄惁鎴愬姛</returns>
+ private bool ParseAndWrite(string address, string value, out string msg)
+ {
+ msg = null;
+
+ try
+ {
+ // 瑙f瀽鍦板潃鏍煎紡: DBx.DBXy.z (BOOL) 鎴� DBx.DBWy (INT/REAL)
+ if (address.Contains(".DBX"))
+ {
+ // BOOL 绫诲瀷: DB1.DBX0.0
+ if (bool.TryParse(value, out bool boolVal))
+ {
+ var addrParts = ParseBoolAddress(address);
+ return _driver.Simens_S7_WriteBool(addrParts.db, addrParts.startAddr, addrParts.pos, boolVal, out msg);
+ }
+ throw new ArgumentException($"[S7] BOOL 鍊艰В鏋愬け璐�: {value}");
+ }
+ else if (address.Contains(".DBW"))
+ {
+ // 灏濊瘯瑙f瀽涓轰笉鍚岀被鍨�
+ if (float.TryParse(value, out float realVal))
+ {
+ var addrParts = ParseWordAddress(address);
+ return _driver.Simens_S7_WriteReal(addrParts.db, addrParts.startAddr, realVal, out msg);
+ }
+ else if (short.TryParse(value, out short shortVal))
+ {
+ var addrParts = ParseWordAddress(address);
+ return _driver.Simens_S7_WriteInt(addrParts.db, addrParts.startAddr, shortVal, out msg);
+ }
+ throw new ArgumentException($"[S7] 鍊艰В鏋愬け璐�: {value}");
+ }
+ else
+ {
+ msg = $"[S7] 涓嶆敮鎸佺殑鍦板潃鏍煎紡: {address}";
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ msg = ex.Message;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// 瑙f瀽鍦板潃骞舵墽琛岃鍙栨搷浣�
+ /// </summary>
+ /// <param name="address">S7 鍦板潃锛屽 "DB1.DBX0.0" 鎴� "DB1.DBW10"</param>
+ /// <returns>璇诲彇鐨勫�煎瓧绗︿覆</returns>
+ private string ParseAndRead(string address)
+ {
+ if (address.Contains(".DBX"))
+ {
+ // BOOL 绫诲瀷璇诲彇
+ var addrParts = ParseBoolAddress(address);
+ if (_driver.Simens_S7_ReadBool(addrParts.db, addrParts.startAddr, addrParts.pos, out bool value, out string msg))
+ {
+ return value.ToString();
+ }
+ throw new InvalidOperationException($"[S7] 璇诲彇澶辫触: {msg}");
+ }
+ else if (address.Contains(".DBD"))
+ {
+ // REAL 绫诲瀷璇诲彇 (DBD = Double Word)
+ var addrParts = ParseWordAddress(address);
+ if (_driver.Simens_S7_ReadReal(addrParts.db, addrParts.startAddr, out float realValue, out string msg))
+ {
+ return realValue.ToString();
+ }
+ throw new InvalidOperationException($"[S7] 璇诲彇澶辫触: {msg}");
+ }
+ else if (address.Contains(".DBW"))
+ {
+ // INT 绫诲瀷璇诲彇
+ var addrParts = ParseWordAddress(address);
+ if (_driver.Simens_S7_ReadInt(addrParts.db, addrParts.startAddr, out int value, out string msg))
+ {
+ return value.ToString();
+ }
+ throw new InvalidOperationException($"[S7] 璇诲彇澶辫触: {msg}");
+ }
+ else
+ {
+ throw new ArgumentException($"[S7] 涓嶆敮鎸佺殑鍦板潃鏍煎紡: {address}");
+ }
+ }
+
+ /// <summary>
+ /// 瑙f瀽 BOOL 鍦板潃鏍煎紡: DBx.DBXy.z
+ /// </summary>
+ private (int db, int startAddr, int pos) ParseBoolAddress(string address)
+ {
+ // 鏍煎紡: DB1.DBX0.0
+ string upper = address.ToUpper();
+ int dbStart = upper.IndexOf("DB") + 2;
+ int dbxStart = upper.IndexOf(".DBX");
+ int db = int.Parse(upper.Substring(dbStart, dbxStart - dbStart));
+
+ int posStart = dbxStart + 5; // .DBX 鍚�
+ int dotPos = address.IndexOf('.', posStart);
+ int startAddr = int.Parse(address.Substring(posStart, dotPos - posStart));
+ int pos = int.Parse(address.Substring(dotPos + 1));
+
+ return (db, startAddr, pos);
+ }
+
+ /// <summary>
+ /// 瑙f瀽 Word 鍦板潃鏍煎紡: DBx.DBWy 鎴� DBx.DBDy
+ /// </summary>
+ private (int db, int startAddr) ParseWordAddress(string address)
+ {
+ // 鏍煎紡: DB1.DBW10 鎴� DB1.DBD0
+ string upper = address.ToUpper();
+ int dbStart = upper.IndexOf("DB") + 2;
+ int dbwStart = upper.Contains(".DBW") ? upper.IndexOf(".DBW") : upper.IndexOf(".DBD");
+ int db = int.Parse(upper.Substring(dbStart, dbwStart - dbStart));
+
+ int addrStart = dbwStart + 5; // .DBW 鎴� .DBD 鍚�
+ int startAddr = int.Parse(address.Substring(addrStart));
+
+ return (db, startAddr);
+ }
+
+ /// <summary>
+ /// 瑙﹀彂杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ private void OnConnectionChanged(bool isConnected, string reason)
+ {
+ ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
+ {
+ DeviceId = DeviceId,
+ IsConnected = isConnected,
+ Reason = reason
+ });
+
+ if (!isConnected)
+ {
+ _reconnectCount++;
+ }
+ }
+
+ /// <summary>
+ /// 妫�鏌ユ槸鍚﹀凡閲婃斁
+ /// </summary>
+ private void ThrowIfDisposed()
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(S7DriverAdapter));
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs b/Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs
new file mode 100644
index 0000000..c4cde19
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/ScpiDriverAdapter.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using log4net;
+using PLCHelper;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// SCPI 鍗忚椹卞姩閫傞厤鍣ㄣ��
+ /// 灏佽 PLCHelper.RestTcpClient锛屽疄鐜扮粺涓�鐨� IDeviceDriver 鎺ュ彛銆�
+ /// 鐢ㄤ簬鎺у埗鏀寔 SCPI 鍛戒护鐨勪华鍣紙濡傜數婧愩�佺數瀛愯礋杞界瓑锛夈��
+ /// </summary>
+ public sealed class ScpiDriverAdapter : IDeviceDriver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(ScpiDriverAdapter));
+
+ private readonly string _ip;
+ private readonly int _port;
+ private RestTcpClient? _client;
+ private bool _disposed;
+ private readonly Stopwatch _latencyStopwatch = new Stopwatch();
+ private double _lastLatencyMs;
+ private string _lastError = string.Empty;
+ private DateTime? _lastErrorTime;
+ private int _reconnectCount;
+
+ /// <inheritdoc/>
+ public string DeviceId { get; }
+
+ /// <inheritdoc/>
+ public bool IsConnected => _client != null;
+
+ /// <inheritdoc/>
+ public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+
+ /// <summary>
+ /// 鍒濆鍖� SCPI 鍗忚椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="ip">鐩爣 IP 鍦板潃</param>
+ /// <param name="port">鐩爣绔彛鍙�</param>
+ public ScpiDriverAdapter(string deviceId, string ip, int port)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ _ip = ip ?? throw new ArgumentNullException(nameof(ip));
+ _port = port;
+ }
+
+ /// <inheritdoc/>
+ public Task ConnectAsync(CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ Log.Info($"[SCPI] 璁惧 {DeviceId} 姝e湪杩炴帴 {_ip}:{_port}...");
+
+ try
+ {
+ _client = new RestTcpClient(_ip, _port);
+ _client.ReceiveTimeout = 5000;
+ _client.ConnectServer();
+
+ // 鍙戦�佸浣嶅懡浠ら獙璇佽繛鎺�
+ if (_client.Request("*RST", out string response, out string errMsg))
+ {
+ Log.Info($"[SCPI] 璁惧 {DeviceId} 澶嶄綅鍝嶅簲: {response}");
+ }
+ else
+ {
+ Log.Warn($"[SCPI] 璁惧 {DeviceId} 澶嶄綅澶辫触: {errMsg}");
+ }
+
+ _lastError = string.Empty;
+ Log.Info($"[SCPI] 璁惧 {DeviceId} 宸叉垚鍔熻繛鎺ュ埌 {_ip}:{_port}");
+ OnConnectionChanged(true, "杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[SCPI] 璁惧 {DeviceId} 杩炴帴寮傚父: {ex.Message}", ex);
+ _client?.Dispose();
+ _client = null;
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task DisconnectAsync()
+ {
+ ThrowIfDisposed();
+ Log.Info($"[SCPI] 璁惧 {DeviceId} 姝e湪鏂紑杩炴帴...");
+
+ try
+ {
+ _client?.Dispose();
+ _client = null;
+
+ Log.Info($"[SCPI] 璁惧 {DeviceId} 宸叉柇寮�杩炴帴");
+ OnConnectionChanged(false, "涓诲姩鏂紑");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[SCPI] 璁惧 {DeviceId} 鏂紑杩炴帴寮傚父: {ex.Message}", ex);
+ _client = null;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public Task<string> SendCommandAsync(string command, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[SCPI] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[SCPI] 璁惧 {DeviceId} 鍙戦�佸懡浠�: {command}");
+
+ // SCPI 鍛戒护浠� \r 缁撳熬锛圧estTcpClient 鍗忚鑷姩澶勭悊锛�
+ bool success = _client.Request(command, out string response, out string errMsg);
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+
+ if (!success)
+ {
+ _lastError = errMsg;
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[SCPI] 鍛戒护鎵ц澶辫触: {errMsg}");
+ }
+
+ Log.Debug($"[SCPI] 璁惧 {DeviceId} 鍛戒护鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[SCPI] 璁惧 {DeviceId} 鍙戦�佸懡浠ゅけ璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task<string> QueryAsync(string query, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[SCPI] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[SCPI] 璁惧 {DeviceId} 鍙戦�佹煡璇�: {query}");
+
+ // 鍙戦�佹煡璇㈠懡浠ゅ苟绛夊緟鍥炶锛堝 "MEAS:CURR?"锛�
+ bool success = _client.Request(query, out string response, out string errMsg);
+
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+
+ if (!success)
+ {
+ _lastError = errMsg;
+ _lastErrorTime = DateTime.Now;
+ throw new TimeoutException($"[SCPI] 鏌ヨ瓒呮椂鎴栧け璐�: {errMsg}");
+ }
+
+ Log.Debug($"[SCPI] 璁惧 {DeviceId} 鏌ヨ鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return Task.FromResult(response);
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[SCPI] 璁惧 {DeviceId} 鏌ヨ澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus()
+ {
+ return new DeviceDriverStatus
+ {
+ IsConnected = IsConnected,
+ IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
+ LastError = _lastError,
+ LastErrorTime = _lastErrorTime,
+ LatencyMs = _lastLatencyMs,
+ ReconnectCount = _reconnectCount
+ };
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ _client?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[SCPI] 璁惧 {DeviceId} 閲婃斁璧勬簮寮傚父: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ private void OnConnectionChanged(bool isConnected, string reason)
+ {
+ ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
+ {
+ DeviceId = DeviceId,
+ IsConnected = isConnected,
+ Reason = reason
+ });
+
+ if (!isConnected)
+ {
+ _reconnectCount++;
+ }
+ }
+
+ /// <summary>
+ /// 妫�鏌ユ槸鍚﹀凡閲婃斁
+ /// </summary>
+ private void ThrowIfDisposed()
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(ScpiDriverAdapter));
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs b/Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs
new file mode 100644
index 0000000..4599d6d
--- /dev/null
+++ b/Aging.Infrastructure/Drivers/TcpRawDriverAdapter.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Diagnostics;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using ConnectionUtil;
+using log4net;
+
+namespace Aging.Infrastructure.Drivers
+{
+ /// <summary>
+ /// TCP 鍘熷閫氫俊椹卞姩閫傞厤鍣ㄣ��
+ /// 灏佽 ConnectionUtil.TcpClientWrapper锛屽疄鐜扮粺涓�鐨� IDeviceDriver 鎺ュ彛銆�
+ /// </summary>
+ public sealed class TcpRawDriverAdapter : IDeviceDriver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(TcpRawDriverAdapter));
+
+ private readonly string _ip;
+ private readonly int _port;
+ private TcpClientWrapper? _client;
+ private bool _disposed;
+ private readonly Stopwatch _latencyStopwatch = new Stopwatch();
+ private double _lastLatencyMs;
+ private string _lastError = string.Empty;
+ private DateTime? _lastErrorTime;
+ private int _reconnectCount;
+
+ /// <inheritdoc/>
+ public string DeviceId { get; }
+
+ /// <inheritdoc/>
+ public bool IsConnected => _client?.IsConnected ?? false;
+
+ /// <inheritdoc/>
+ public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged;
+
+ /// <summary>
+ /// 鍒濆鍖� TCP 鍘熷閫氫俊椹卞姩閫傞厤鍣�
+ /// </summary>
+ /// <param name="deviceId">璁惧鍞竴鏍囪瘑绗�</param>
+ /// <param name="ip">鐩爣 IP 鍦板潃</param>
+ /// <param name="port">鐩爣绔彛鍙�</param>
+ public TcpRawDriverAdapter(string deviceId, string ip, int port)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ _ip = ip ?? throw new ArgumentNullException(nameof(ip));
+ _port = port;
+ }
+
+ /// <inheritdoc/>
+ public async Task ConnectAsync(CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ Log.Info($"[TCP] 璁惧 {DeviceId} 姝e湪杩炴帴 {_ip}:{_port}...");
+
+ try
+ {
+ _client = new TcpClientWrapper(_ip, _port);
+ bool success = await _client.ConnectAsync();
+
+ if (!success)
+ {
+ _lastError = "杩炴帴澶辫触";
+ _lastErrorTime = DateTime.Now;
+ throw new InvalidOperationException($"[TCP] 璁惧 {DeviceId} 杩炴帴 {_ip}:{_port} 澶辫触");
+ }
+
+ _lastError = string.Empty;
+ Log.Info($"[TCP] 璁惧 {DeviceId} 宸叉垚鍔熻繛鎺ュ埌 {_ip}:{_port}");
+ OnConnectionChanged(true, "杩炴帴鎴愬姛");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[TCP] 璁惧 {DeviceId} 杩炴帴寮傚父: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public Task DisconnectAsync()
+ {
+ ThrowIfDisposed();
+ Log.Info($"[TCP] 璁惧 {DeviceId} 姝e湪鏂紑杩炴帴...");
+
+ try
+ {
+ if (_client != null)
+ {
+ _client?.Close();
+ _client = null;
+ }
+
+ Log.Info($"[TCP] 璁惧 {DeviceId} 宸叉柇寮�杩炴帴");
+ OnConnectionChanged(false, "涓诲姩鏂紑");
+ }
+ catch (Exception ex)
+ {
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[TCP] 璁惧 {DeviceId} 鏂紑杩炴帴寮傚父: {ex.Message}", ex);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// <inheritdoc/>
+ public async Task<string> SendCommandAsync(string command, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[TCP] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[TCP] 璁惧 {DeviceId} 鍙戦�佸懡浠�: {command}");
+ string response = await _client.SendMessageAsync(command);
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ Log.Debug($"[TCP] 璁惧 {DeviceId} 鏀跺埌鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return response;
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[TCP] 璁惧 {DeviceId} 鍙戦�佸懡浠ゅけ璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task<string> QueryAsync(string query, CancellationToken ct)
+ {
+ ThrowIfDisposed();
+ if (!IsConnected) throw new InvalidOperationException("[TCP] 璁惧鏈繛鎺�");
+
+ _latencyStopwatch.Restart();
+ try
+ {
+ Log.Debug($"[TCP] 璁惧 {DeviceId} 鍙戦�佹煡璇�: {query}");
+ // 浣跨敤杈冪煭瓒呮椂杩涜鏌ヨ鎿嶄綔
+ string response = await _client.SendMessageAsync(query, TimeSpan.FromSeconds(5));
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ Log.Debug($"[TCP] 璁惧 {DeviceId} 鏌ヨ鍝嶅簲: {response} (寤惰繜 {_lastLatencyMs:F2}ms)");
+ return response;
+ }
+ catch (Exception ex)
+ {
+ _latencyStopwatch.Stop();
+ _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds;
+ _lastError = ex.Message;
+ _lastErrorTime = DateTime.Now;
+ Log.Error($"[TCP] 璁惧 {DeviceId} 鏌ヨ澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public DeviceDriverStatus GetStatus()
+ {
+ return new DeviceDriverStatus
+ {
+ IsConnected = IsConnected,
+ IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError),
+ LastError = _lastError,
+ LastErrorTime = _lastErrorTime,
+ LatencyMs = _lastLatencyMs,
+ ReconnectCount = _reconnectCount
+ };
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ try
+ {
+ _client?.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[TCP] 璁惧 {DeviceId} 閲婃斁璧勬簮寮傚父: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 瑙﹀彂杩炴帴鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ private void OnConnectionChanged(bool isConnected, string reason)
+ {
+ ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs
+ {
+ DeviceId = DeviceId,
+ IsConnected = isConnected,
+ Reason = reason
+ });
+
+ if (!isConnected)
+ {
+ _reconnectCount++;
+ }
+ }
+
+ /// <summary>
+ /// 妫�鏌ユ槸鍚﹀凡閲婃斁
+ /// </summary>
+ private void ThrowIfDisposed()
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(TcpRawDriverAdapter));
+ }
+ }
+}
diff --git a/Aging.Infrastructure/InfrastructureBootstrapper.cs b/Aging.Infrastructure/InfrastructureBootstrapper.cs
new file mode 100644
index 0000000..aebb5b0
--- /dev/null
+++ b/Aging.Infrastructure/InfrastructureBootstrapper.cs
@@ -0,0 +1,152 @@
+using log4net;
+using HMI.Core.Util;
+using Aging.Infrastructure.Contracts;
+using Aging.Infrastructure.Core;
+using Aging.Infrastructure.Services;
+using Aging.Infrastructure.Drivers;
+
+namespace Aging.Infrastructure
+{
+ /// <summary>
+ /// 鍩虹璁炬柦鍚姩寮曞 - 鍒濆鍖栧苟娉ㄥ唽鎵�鏈夋牳蹇冩湇鍔″埌 UtilContainer
+ /// </summary>
+ public static class InfrastructureBootstrapper
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(InfrastructureBootstrapper));
+ private static bool _initialized = false;
+
+ /// <summary>
+ /// 鍒濆鍖栧熀纭�璁炬柦鏈嶅姟
+ /// 搴斿湪 WPF 涓荤▼搴忓惎鍔ㄦ椂鎴栨彃浠� InitFinish 涓皟鐢�
+ /// </summary>
+ /// <param name="deviceConfigDbPath">DeviceConfig 鎻掍欢鐨� SQLite 鏁版嵁搴撹矾寰�</param>
+ public static void Initialize(string deviceConfigDbPath)
+ {
+ if (_initialized)
+ {
+ log.Warn("鍩虹璁炬柦宸插垵濮嬪寲锛岃烦杩囬噸澶嶅垵濮嬪寲");
+ return;
+ }
+
+ try
+ {
+ log.Info("寮�濮嬪垵濮嬪寲鍩虹璁炬柦鏈嶅姟...");
+
+ // 1. 鍒涘缓 ResourceManager 鍗曚緥
+ var resourceManager = new ResourceManager();
+ UtilContainer.RegisterUtil("ResourceManager", resourceManager);
+ log.Info("ResourceManager 宸叉敞鍐�");
+
+ // 2. 鍒涘缓 DeviceManager 鍗曚緥
+ var deviceManager = new DeviceManager();
+ UtilContainer.RegisterUtil("DeviceManager", deviceManager);
+ log.Info("DeviceManager 宸叉敞鍐�");
+
+ // 3. 鍒涘缓 SlotDeviceMappingResolver锛堥渶瑕� DeviceConfig DB 璺緞锛�
+ var mappingResolver = new SlotDeviceMappingResolver(deviceConfigDbPath);
+ UtilContainer.RegisterUtil("SlotDeviceMappingResolver", mappingResolver);
+ log.Info("SlotDeviceMappingResolver 宸叉敞鍐�");
+
+ // 4. 鍒涘缓 DeviceService
+ var deviceService = new DeviceService(resourceManager, deviceManager, mappingResolver);
+ UtilContainer.RegisterUtil("DeviceService", deviceService);
+ log.Info("DeviceService 宸叉敞鍐�");
+
+ // 5. 鍒涘缓 PowerSupplyService
+ var powerSupplyService = new PowerSupplyService(resourceManager, deviceManager, mappingResolver);
+ UtilContainer.RegisterUtil("PowerSupplyService", powerSupplyService);
+ log.Info("PowerSupplyService 宸叉敞鍐�");
+
+ // 6. 浠庢暟鎹簱鍔犺浇璁惧瀹氫箟骞舵敞鍐岄┍鍔�
+ LoadDevicesFromConfig(deviceManager, mappingResolver);
+
+ _initialized = true;
+ log.Info("鍩虹璁炬柦鏈嶅姟鍒濆鍖栧畬鎴�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍩虹璁炬柦鍒濆鍖栧け璐�", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 浠� DeviceConfig 鏁版嵁搴撳姞杞借澶囧畾涔夊苟娉ㄥ唽椹卞姩
+ /// </summary>
+ private static void LoadDevicesFromConfig(IDeviceManager deviceManager, SlotDeviceMappingResolver mappingResolver)
+ {
+ try
+ {
+ // 璇诲彇鎵�鏈夎澶囨槧灏�
+ var mappings = mappingResolver.GetAllMappings();
+ // 浠庢槧灏勪腑鎻愬彇鍞竴鐨勮澶嘔D
+ var deviceIds = mappings
+ .Select(m => m.DeviceId)
+ .Distinct()
+ .ToList();
+
+ int loaded = 0;
+ foreach (var deviceId in deviceIds)
+ {
+ try
+ {
+ // 杩欓噷鏆傛椂浣跨敤閫氱敤 TCP 椹卞姩浣滀负鍗犱綅
+ // 瀹為檯搴旇浠� device_definition 琛ㄨ鍙� transport 鍜� endpoint
+ // 鐒跺悗閫氳繃 DriverFactory.CreateDriver 鍒涘缓瀵瑰簲椹卞姩
+ //
+ // TODO: 浠庢暟鎹簱璇诲彇璁惧璇︾粏淇℃伅锛坱ransport, endpoint锛�
+ // var device = GetDeviceDefinition(deviceId);
+ // var driver = DriverFactory.CreateDriver(deviceId, device.Transport, device.Endpoint);
+
+ log.Debug($"璁惧 {deviceId} 椹卞姩娉ㄥ唽寰呴厤缃缁嗕俊鎭�");
+ loaded++;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"娉ㄥ唽璁惧 {deviceId} 椹卞姩澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ log.Info($"浠庤澶囬厤缃姞杞戒簡 {loaded} 涓澶囬┍鍔�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍔犺浇璁惧閰嶇疆澶辫触", ex);
+ }
+ }
+
+ /// <summary>
+ /// 娉ㄥ唽鍗曚釜璁惧椹卞姩锛堜緵澶栭儴璋冪敤锛岀敤浜庡姩鎬佹坊鍔犺澶囷級
+ /// </summary>
+ public static void RegisterDevice(string deviceId, string transport, string endpoint)
+ {
+ var container = new UtilContainer();
+ var deviceManager = container.GetUtil<IDeviceManager>("DeviceManager");
+ if (deviceManager == null)
+ {
+ log.Error("DeviceManager 鏈垵濮嬪寲");
+ return;
+ }
+
+ try
+ {
+ var driver = DriverFactory.CreateDriver(deviceId, transport, endpoint);
+ deviceManager.RegisterDriver(deviceId, driver);
+ log.Info($"璁惧 {deviceId} ({transport}) 椹卞姩宸叉敞鍐�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"娉ㄥ唽璁惧 {deviceId} 澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 閲嶇疆鍩虹璁炬柦锛堢敤浜庢祴璇曟垨閲嶆柊鍒濆鍖栵級
+ /// </summary>
+ public static void Reset()
+ {
+ _initialized = false;
+ log.Info("鍩虹璁炬柦宸查噸缃�");
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Services/DeviceService.cs b/Aging.Infrastructure/Services/DeviceService.cs
new file mode 100644
index 0000000..2902549
--- /dev/null
+++ b/Aging.Infrastructure/Services/DeviceService.cs
@@ -0,0 +1,331 @@
+using System.Diagnostics;
+using Aging.Infrastructure.Contracts;
+using log4net;
+
+namespace Aging.Infrastructure.Services
+{
+ /// <summary>
+ /// 璁惧鏈嶅姟瀹炵幇銆�
+ /// 鍗忚皟 ResourceManager銆丏eviceManager 鍜� SlotMapping锛�
+ /// 鎻愪緵闈㈠悜 DeviceId 鐨勫懡浠ゆ墽琛屻�佹煡璇㈠拰瀹夊叏鍏虫満鍔熻兘銆�
+ /// 鎵�鏈夋搷浣滃潎闇�閫氳繃璧勬簮棰勭害楠岃瘉鍜岀绾︾鐞嗭紝纭繚纭欢璧勬簮鐨勫畨鍏ㄨ闂��
+ /// </summary>
+ public sealed class DeviceService : IDeviceService
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceService));
+
+ /// <summary>
+ /// 璧勬簮绠$悊鍣�
+ /// </summary>
+ private readonly IResourceManager _resourceManager;
+
+ /// <summary>
+ /// 璁惧绠$悊鍣�
+ /// </summary>
+ private readonly IDeviceManager _deviceManager;
+
+ /// <summary>
+ /// Slot-璁惧-閫氶亾鏄犲皠瑙f瀽鍣�
+ /// </summary>
+ private readonly SlotDeviceMappingResolver _slotMappingResolver;
+
+ /// <summary>
+ /// 鍒濆鍖栬澶囨湇鍔″疄渚�
+ /// </summary>
+ /// <param name="resourceManager">璧勬簮绠$悊鍣�</param>
+ /// <param name="deviceManager">璁惧绠$悊鍣�</param>
+ /// <param name="slotMappingResolver">Slot 鏄犲皠瑙f瀽鍣�</param>
+ public DeviceService(
+ IResourceManager resourceManager,
+ IDeviceManager deviceManager,
+ SlotDeviceMappingResolver slotMappingResolver)
+ {
+ _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
+ _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager));
+ _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver));
+ }
+
+ /// <inheritdoc/>
+ public async Task<DeviceCommandResult> ExecuteCommandAsync(
+ string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+ if (string.IsNullOrWhiteSpace(command))
+ throw new ArgumentException("鍛戒护涓嶈兘涓虹┖", nameof(command));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var stopwatch = Stopwatch.StartNew();
+ var resourceKey = new ResourceKey(deviceId, "Generic");
+
+ try
+ {
+ Log.Info($"[DeviceService] 鎵ц鍛戒护: Device={deviceId}, Command={command}, Owner={owner.OwnerId}");
+
+ // 1. 鑾峰彇璁惧椹卞姩
+ var driver = _deviceManager.GetDriver(deviceId);
+ if (driver == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 璁惧 {deviceId} 椹卞姩涓嶅瓨鍦�");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "DRIVER_NOT_FOUND",
+ ErrorMessage = $"璁惧 {deviceId} 鐨勯┍鍔ㄦ湭娉ㄥ唽",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 鏉冮檺楠岃瘉澶辫触: Device={deviceId}, Owner={owner.OwnerId}");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "PERMISSION_DENIED",
+ ErrorMessage = $"鎵�鏈夎�� {owner.OwnerId} 鏈绾﹁澶� {deviceId} 鎴栦护鐗屽凡杩囨湡",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+
+ // 3. 鑾峰彇绉熺害骞舵墽琛屽懡浠�
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ var response = await driver.SendCommandAsync(command, ct).ConfigureAwait(false);
+
+ stopwatch.Stop();
+ Log.Info($"[DeviceService] 鍛戒护鎵ц鎴愬姛: Device={deviceId}, Command={command}, 寤惰繜={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
+
+ return new DeviceCommandResult
+ {
+ Success = true,
+ RawResponse = response,
+ Readback = response,
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (ResourceConflictException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 璧勬簮鍐茬獊: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "RESOURCE_CONFLICT",
+ ErrorMessage = ex.Message,
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (OperationCanceledException)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 鍛戒护鎵ц琚彇娑�: Device={deviceId}");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "OPERATION_CANCELLED",
+ ErrorMessage = "鎿嶄綔宸茶鍙栨秷",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (TimeoutException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 鍛戒护鎵ц瓒呮椂: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "TIMEOUT",
+ ErrorMessage = $"璁惧 {deviceId} 鍛戒护鎵ц瓒呮椂: {ex.Message}",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 鍛戒护鎵ц寮傚父: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "UNKNOWN_ERROR",
+ ErrorMessage = $"璁惧 {deviceId} 鍛戒护鎵ц澶辫触: {ex.Message}",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task<DeviceCommandResult> ExecuteQueryAsync(
+ string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+ if (string.IsNullOrWhiteSpace(query))
+ throw new ArgumentException("鏌ヨ鍛戒护涓嶈兘涓虹┖", nameof(query));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var stopwatch = Stopwatch.StartNew();
+ var resourceKey = new ResourceKey(deviceId, "Generic");
+
+ try
+ {
+ Log.Info($"[DeviceService] 鎵ц鏌ヨ: Device={deviceId}, Query={query}, Owner={owner.OwnerId}");
+
+ // 1. 鑾峰彇璁惧椹卞姩
+ var driver = _deviceManager.GetDriver(deviceId);
+ if (driver == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 璁惧 {deviceId} 椹卞姩涓嶅瓨鍦�");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "DRIVER_NOT_FOUND",
+ ErrorMessage = $"璁惧 {deviceId} 鐨勯┍鍔ㄦ湭娉ㄥ唽",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 鏉冮檺楠岃瘉澶辫触: Device={deviceId}, Owner={owner.OwnerId}");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "PERMISSION_DENIED",
+ ErrorMessage = $"鎵�鏈夎�� {owner.OwnerId} 鏈绾﹁澶� {deviceId} 鎴栦护鐗屽凡杩囨湡",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+
+ // 3. 鑾峰彇绉熺害骞舵墽琛屾煡璇�
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ var response = await driver.QueryAsync(query, ct).ConfigureAwait(false);
+
+ stopwatch.Stop();
+ Log.Info($"[DeviceService] 鏌ヨ鎵ц鎴愬姛: Device={deviceId}, Query={query}, 寤惰繜={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
+
+ return new DeviceCommandResult
+ {
+ Success = true,
+ RawResponse = response,
+ Readback = response,
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (ResourceConflictException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 璧勬簮鍐茬獊: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "RESOURCE_CONFLICT",
+ ErrorMessage = ex.Message,
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (OperationCanceledException)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[DeviceService] 鏌ヨ鎵ц琚彇娑�: Device={deviceId}");
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "OPERATION_CANCELLED",
+ ErrorMessage = "鎿嶄綔宸茶鍙栨秷",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (TimeoutException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 鏌ヨ鎵ц瓒呮椂: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "TIMEOUT",
+ ErrorMessage = $"璁惧 {deviceId} 鏌ヨ鎵ц瓒呮椂: {ex.Message}",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[DeviceService] 鏌ヨ鎵ц寮傚父: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ return new DeviceCommandResult
+ {
+ Success = false,
+ ErrorCode = "UNKNOWN_ERROR",
+ ErrorMessage = $"璁惧 {deviceId} 鏌ヨ鎵ц澶辫触: {ex.Message}",
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ }
+
+ /// <inheritdoc/>
+ public async Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(deviceId))
+ throw new ArgumentException("璁惧ID涓嶈兘涓虹┖", nameof(deviceId));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var resourceKey = new ResourceKey(deviceId, "Generic");
+
+ try
+ {
+ Log.Info($"[DeviceService] 寮�濮嬪畨鍏ㄥ叧鏈�: Device={deviceId}, Owner={owner.OwnerId}");
+
+ // 1. 鑾峰彇椹卞姩
+ var driver = _deviceManager.GetDriver(deviceId);
+ if (driver == null)
+ {
+ Log.Warn($"[DeviceService] 瀹夊叏鍏虫満澶辫触: 璁惧 {deviceId} 椹卞姩涓嶅瓨鍦�");
+ return;
+ }
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ Log.Warn($"[DeviceService] 瀹夊叏鍏虫満鏉冮檺楠岃瘉澶辫触: Device={deviceId}, Owner={owner.OwnerId}");
+ return;
+ }
+
+ // 3. 鑾峰彇绉熺害
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ // 4. 鍙戦�佸畨鍏ㄥ叧闂懡浠�
+ Log.Info($"[DeviceService] 鍙戦�佸畨鍏ㄥ叧闂懡浠�: Device={deviceId}");
+ await driver.SendCommandAsync("OUTP OFF", ct).ConfigureAwait(false);
+
+ // 5. 绛夊緟纭锛堢煭鏆傚欢杩熺‘淇濆懡浠ゆ墽琛屽畬鎴愶級
+ await Task.Delay(500, ct).ConfigureAwait(false);
+
+ // 6. 楠岃瘉鍏抽棴鐘舵��
+ var statusResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false);
+ Log.Info($"[DeviceService] 璁惧 {deviceId} 鍏抽棴鐘舵�佺‘璁�: {statusResponse}");
+
+ Log.Info($"[DeviceService] 瀹夊叏鍏虫満瀹屾垚: Device={deviceId}");
+ }
+ catch (OperationCanceledException)
+ {
+ Log.Warn($"[DeviceService] 瀹夊叏鍏虫満鎿嶄綔琚彇娑�: Device={deviceId}");
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[DeviceService] 瀹夊叏鍏虫満寮傚父: Device={deviceId}, 閿欒: {ex.Message}", ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Services/PowerSupplyService.cs b/Aging.Infrastructure/Services/PowerSupplyService.cs
new file mode 100644
index 0000000..693a312
--- /dev/null
+++ b/Aging.Infrastructure/Services/PowerSupplyService.cs
@@ -0,0 +1,543 @@
+using System.Diagnostics;
+using Aging.Infrastructure.Contracts;
+using log4net;
+
+namespace Aging.Infrastructure.Services
+{
+ /// <summary>
+ /// 鐢垫簮閰嶇疆鍛戒护
+ /// </summary>
+ public sealed class PowerCommand
+ {
+ /// <summary>
+ /// 鐩爣鐢靛帇鍊硷紙鍙�夛紝鍗曚綅: V锛�
+ /// </summary>
+ public double? Voltage { get; set; }
+
+ /// <summary>
+ /// 鐩爣鐢垫祦闄愬埗鍊硷紙鍙�夛紝鍗曚綅: A锛�
+ /// </summary>
+ public double? CurrentLimit { get; set; }
+
+ /// <summary>
+ /// 杈撳嚭寮�鍏崇姸鎬侊紙true=ON, false=OFF, null=涓嶆敼鍙橈級
+ /// </summary>
+ public bool? Output { get; set; }
+ }
+
+ /// <summary>
+ /// 鐢垫簮鎿嶄綔缁撴灉
+ /// </summary>
+ public sealed class PowerOperationResult
+ {
+ /// <summary>
+ /// 鎿嶄綔鏄惁鎴愬姛
+ /// </summary>
+ public bool Success { get; set; }
+
+ /// <summary>
+ /// 瀹炴祴鐢靛帇鍊硷紙鍗曚綅: V锛�
+ /// </summary>
+ public double? MeasuredVoltage { get; set; }
+
+ /// <summary>
+ /// 瀹炴祴鐢垫祦鍊硷紙鍗曚綅: A锛�
+ /// </summary>
+ public double? MeasuredCurrent { get; set; }
+
+ /// <summary>
+ /// 璁惧鍘熷鍝嶅簲瀛楃涓�
+ /// </summary>
+ public string? RawResponse { get; set; }
+
+ /// <summary>
+ /// 閿欒浠g爜
+ /// </summary>
+ public string? ErrorCode { get; set; }
+
+ /// <summary>
+ /// 閿欒淇℃伅
+ /// </summary>
+ public string? ErrorMessage { get; set; }
+
+ /// <summary>
+ /// 鎿嶄綔寤惰繜锛堟绉掞級
+ /// </summary>
+ public double LatencyMs { get; set; }
+
+ /// <summary>
+ /// 鍒涘缓涓�涓垚鍔熺殑缁撴灉
+ /// </summary>
+ public static PowerOperationResult Succeed(double? voltage, double? current, string? rawResponse, double latencyMs)
+ {
+ return new PowerOperationResult
+ {
+ Success = true,
+ MeasuredVoltage = voltage,
+ MeasuredCurrent = current,
+ RawResponse = rawResponse,
+ LatencyMs = latencyMs
+ };
+ }
+
+ /// <summary>
+ /// 鍒涘缓涓�涓け璐ョ殑缁撴灉
+ /// </summary>
+ public static PowerOperationResult Failed(string errorCode, string errorMessage, double latencyMs)
+ {
+ return new PowerOperationResult
+ {
+ Success = false,
+ ErrorCode = errorCode,
+ ErrorMessage = errorMessage,
+ LatencyMs = latencyMs
+ };
+ }
+ }
+
+ /// <summary>
+ /// 鐢垫簮閰嶇疆鏈嶅姟 - 闈㈠悜 Slot 鐨勯珮灞傜數婧愭搷浣溿��
+ /// 閫氳繃 SlotDeviceMappingResolver 瑙f瀽 Slot 鍒板叿浣撹澶囩鐐圭殑鏄犲皠锛�
+ /// 鍗忚皟 ResourceManager 鍜� DeviceManager 瀹屾垚鐢垫簮閰嶇疆銆佹祴閲忓拰鐘舵�佹煡璇€��
+ /// </summary>
+ public sealed class PowerSupplyService
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(PowerSupplyService));
+
+ /// <summary>
+ /// 璧勬簮绠$悊鍣�
+ /// </summary>
+ private readonly IResourceManager _resourceManager;
+
+ /// <summary>
+ /// 璁惧绠$悊鍣�
+ /// </summary>
+ private readonly IDeviceManager _deviceManager;
+
+ /// <summary>
+ /// Slot-璁惧-閫氶亾鏄犲皠瑙f瀽鍣�
+ /// </summary>
+ private readonly SlotDeviceMappingResolver _slotMappingResolver;
+
+ /// <summary>
+ /// 鍒濆鍖栫數婧愰厤缃湇鍔″疄渚�
+ /// </summary>
+ /// <param name="resourceManager">璧勬簮绠$悊鍣�</param>
+ /// <param name="deviceManager">璁惧绠$悊鍣�</param>
+ /// <param name="slotMappingResolver">Slot 鏄犲皠瑙f瀽鍣�</param>
+ public PowerSupplyService(
+ IResourceManager resourceManager,
+ IDeviceManager deviceManager,
+ SlotDeviceMappingResolver slotMappingResolver)
+ {
+ _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
+ _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager));
+ _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver));
+ }
+
+ /// <summary>
+ /// 寮傛閰嶇疆 Slot 瀵瑰簲鐨勭數婧愬弬鏁般��
+ /// 鏍规嵁 SlotId 瑙f瀽鏄犲皠鑾峰彇璁惧绔偣锛岄獙璇佽祫婧愰绾﹀悗鍙戦�� SCPI 鍛戒护閰嶇疆鐢垫簮銆�
+ /// </summary>
+ /// <param name="slotId">Slot 鏍囪瘑绗�</param>
+ /// <param name="command">鐢垫簮閰嶇疆鍛戒护</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鐢垫簮鎿嶄綔缁撴灉锛屽寘鍚祴閲忓��</returns>
+ public async Task<PowerOperationResult> ConfigureAsync(
+ string slotId, PowerCommand command, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(slotId))
+ throw new ArgumentException("SlotId 涓嶈兘涓虹┖", nameof(slotId));
+ if (command == null)
+ throw new ArgumentNullException(nameof(command));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ Log.Info($"[PowerSupplyService] 閰嶇疆鐢垫簮: Slot={slotId}, Owner={owner.OwnerId}");
+
+ // 1. 瑙f瀽 Slot 鏄犲皠
+ var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
+ if (endpoint == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] Slot {slotId} 鏈壘鍒� PowerSupply 鏄犲皠");
+ return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
+ $"Slot {slotId} 鏈壘鍒� PowerSupply 绫诲瀷鐨勮澶囨槧灏�", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ var resourceKey = endpoint.ToResourceKey();
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 鏉冮檺楠岃瘉澶辫触: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
+ return PowerOperationResult.Failed("PERMISSION_DENIED",
+ $"鎵�鏈夎�� {owner.OwnerId} 鏈绾� Slot {slotId} 瀵瑰簲鐨勮澶� {endpoint.DeviceId} 鎴栦护鐗屽凡杩囨湡",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 3. 鑾峰彇绉熺害
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ // 4. 鑾峰彇璁惧椹卞姩
+ var driver = _deviceManager.GetDriver(endpoint.DeviceId);
+ if (driver == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 璁惧 {endpoint.DeviceId} 椹卞姩涓嶅瓨鍦�");
+ return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
+ $"璁惧 {endpoint.DeviceId} 鐨勯┍鍔ㄦ湭娉ㄥ唽", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 5. 鏋勫缓骞跺彂閫� SCPI 鍛戒护
+ var scpiCommands = BuildScpiCommands(command);
+ foreach (var scpiCommand in scpiCommands)
+ {
+ Log.Info($"[PowerSupplyService] 鍙戦�� SCPI 鍛戒护: Device={endpoint.DeviceId}, Command={scpiCommand}");
+ await driver.SendCommandAsync(scpiCommand, ct).ConfigureAwait(false);
+ }
+
+ // 6. 鍥炶娴嬮噺鍊�
+ string rawResponse;
+ try
+ {
+ // 浼樺厛灏濊瘯 MEAS:ALL? 涓�娆℃�ц幏鍙栨墍鏈夋祴閲忓��
+ rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false);
+ Log.Info($"[PowerSupplyService] MEAS:ALL? 鍝嶅簲: {rawResponse}");
+ }
+ catch
+ {
+ // 濡傛灉 MEAS:ALL? 澶辫触锛屽垎鍒煡璇㈢數鍘嬪拰鐢垫祦
+ Log.Info($"[PowerSupplyService] MEAS:ALL? 澶辫触锛屽皾璇曞垎鍒煡璇� VOLT? 鍜� CURR?");
+ var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
+ var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
+ rawResponse = $"{voltageResponse},{currentResponse}";
+ }
+
+ // 7. 瑙f瀽鍥炶鍊�
+ ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent);
+
+ stopwatch.Stop();
+ Log.Info($"[PowerSupplyService] 鐢垫簮閰嶇疆瀹屾垚: Slot={slotId}, Device={endpoint.DeviceId}, " +
+ $"V={measuredVoltage}, I={measuredCurrent}, 寤惰繜={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
+
+ return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse,
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (ResourceConflictException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 璧勬簮鍐茬獊: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (OperationCanceledException)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 鐢垫簮閰嶇疆琚彇娑�: Slot={slotId}");
+ return PowerOperationResult.Failed("OPERATION_CANCELLED", "鎿嶄綔宸茶鍙栨秷",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (TimeoutException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 鐢垫簮閰嶇疆瓒呮椂: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("TIMEOUT", $"鐢垫簮閰嶇疆瓒呮椂: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 鐢垫簮閰嶇疆寮傚父: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("UNKNOWN_ERROR", $"鐢垫簮閰嶇疆澶辫触: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ }
+
+ /// <summary>
+ /// 寮傛娴嬮噺 Slot 瀵瑰簲鐢垫簮鐨勮緭鍑哄�硷紙鐢靛帇銆佺數娴侊級銆�
+ /// 鏍规嵁 SlotId 瑙f瀽鏄犲皠鑾峰彇璁惧绔偣锛屾煡璇㈠疄鏃舵祴閲忓�笺��
+ /// </summary>
+ /// <param name="slotId">Slot 鏍囪瘑绗�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鐢垫簮鎿嶄綔缁撴灉锛屽寘鍚祴閲忓��</returns>
+ public async Task<PowerOperationResult> MeasureAsync(
+ string slotId, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(slotId))
+ throw new ArgumentException("SlotId 涓嶈兘涓虹┖", nameof(slotId));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ Log.Info($"[PowerSupplyService] 娴嬮噺鐢垫簮: Slot={slotId}, Owner={owner.OwnerId}");
+
+ // 1. 瑙f瀽 Slot 鏄犲皠
+ var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
+ if (endpoint == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] Slot {slotId} 鏈壘鍒� PowerSupply 鏄犲皠");
+ return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
+ $"Slot {slotId} 鏈壘鍒� PowerSupply 绫诲瀷鐨勮澶囨槧灏�", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ var resourceKey = endpoint.ToResourceKey();
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 鏉冮檺楠岃瘉澶辫触: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
+ return PowerOperationResult.Failed("PERMISSION_DENIED",
+ $"鎵�鏈夎�� {owner.OwnerId} 鏈绾� Slot {slotId} 瀵瑰簲鐨勮澶� {endpoint.DeviceId} 鎴栦护鐗屽凡杩囨湡",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 3. 鑾峰彇绉熺害
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ // 4. 鑾峰彇璁惧椹卞姩
+ var driver = _deviceManager.GetDriver(endpoint.DeviceId);
+ if (driver == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 璁惧 {endpoint.DeviceId} 椹卞姩涓嶅瓨鍦�");
+ return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
+ $"璁惧 {endpoint.DeviceId} 鐨勯┍鍔ㄦ湭娉ㄥ唽", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 5. 鏌ヨ娴嬮噺鍊�
+ string rawResponse;
+ try
+ {
+ rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false);
+ }
+ catch
+ {
+ var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
+ var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
+ rawResponse = $"{voltageResponse},{currentResponse}";
+ }
+
+ // 6. 瑙f瀽鍥炶鍊�
+ ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent);
+
+ stopwatch.Stop();
+ Log.Info($"[PowerSupplyService] 娴嬮噺瀹屾垚: Slot={slotId}, Device={endpoint.DeviceId}, " +
+ $"V={measuredVoltage}, I={measuredCurrent}, 寤惰繜={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
+
+ return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse,
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (ResourceConflictException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 璧勬簮鍐茬獊: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (OperationCanceledException)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 娴嬮噺鎿嶄綔琚彇娑�: Slot={slotId}");
+ return PowerOperationResult.Failed("OPERATION_CANCELLED", "鎿嶄綔宸茶鍙栨秷",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (TimeoutException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 娴嬮噺鎿嶄綔瓒呮椂: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("TIMEOUT", $"娴嬮噺鎿嶄綔瓒呮椂: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 娴嬮噺鎿嶄綔寮傚父: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("UNKNOWN_ERROR", $"娴嬮噺鎿嶄綔澶辫触: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ }
+
+ /// <summary>
+ /// 寮傛鑾峰彇 Slot 瀵瑰簲鐢垫簮鐨勫綋鍓嶇姸鎬侊紙鐢靛帇銆佺數娴併�佽緭鍑哄紑鍏崇姸鎬侊級銆�
+ /// </summary>
+ /// <param name="slotId">Slot 鏍囪瘑绗�</param>
+ /// <param name="owner">鎵�鏈夎�呬护鐗�</param>
+ /// <param name="ct">鍙栨秷浠ょ墝</param>
+ /// <returns>鐢垫簮鎿嶄綔缁撴灉锛屽寘鍚姸鎬佷俊鎭�</returns>
+ public async Task<PowerOperationResult> GetPowerStatusAsync(
+ string slotId, ResourceOwnerToken owner, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(slotId))
+ throw new ArgumentException("SlotId 涓嶈兘涓虹┖", nameof(slotId));
+ if (owner == null)
+ throw new ArgumentNullException(nameof(owner));
+
+ var stopwatch = Stopwatch.StartNew();
+
+ try
+ {
+ Log.Info($"[PowerSupplyService] 鏌ヨ鐢垫簮鐘舵��: Slot={slotId}, Owner={owner.OwnerId}");
+
+ // 1. 瑙f瀽 Slot 鏄犲皠
+ var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply");
+ if (endpoint == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] Slot {slotId} 鏈壘鍒� PowerSupply 鏄犲皠");
+ return PowerOperationResult.Failed("MAPPING_NOT_FOUND",
+ $"Slot {slotId} 鏈壘鍒� PowerSupply 绫诲瀷鐨勮澶囨槧灏�", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ var resourceKey = endpoint.ToResourceKey();
+
+ // 2. 楠岃瘉棰勭害
+ if (!_resourceManager.ValidateOwner(owner, resourceKey))
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 鏉冮檺楠岃瘉澶辫触: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}");
+ return PowerOperationResult.Failed("PERMISSION_DENIED",
+ $"鎵�鏈夎�� {owner.OwnerId} 鏈绾� Slot {slotId} 瀵瑰簲鐨勮澶� {endpoint.DeviceId} 鎴栦护鐗屽凡杩囨湡",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 3. 鑾峰彇绉熺害
+ using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false);
+
+ // 4. 鑾峰彇璁惧椹卞姩
+ var driver = _deviceManager.GetDriver(endpoint.DeviceId);
+ if (driver == null)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 璁惧 {endpoint.DeviceId} 椹卞姩涓嶅瓨鍦�");
+ return PowerOperationResult.Failed("DRIVER_NOT_FOUND",
+ $"璁惧 {endpoint.DeviceId} 鐨勯┍鍔ㄦ湭娉ㄥ唽", stopwatch.Elapsed.TotalMilliseconds);
+ }
+
+ // 5. 鏌ヨ鐢靛帇銆佺數娴佸拰杈撳嚭鐘舵��
+ var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false);
+ var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false);
+ var outputResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false);
+
+ var rawResponse = $"VOLT={voltageResponse},CURR={currentResponse},OUTP={outputResponse}";
+
+ // 6. 瑙f瀽娴嬮噺鍊�
+ ParseMeasurementResponse($"{voltageResponse},{currentResponse}",
+ out var measuredVoltage, out var measuredCurrent);
+
+ stopwatch.Stop();
+ Log.Info($"[PowerSupplyService] 鐢垫簮鐘舵�佹煡璇㈠畬鎴�: Slot={slotId}, Device={endpoint.DeviceId}, " +
+ $"V={measuredVoltage}, I={measuredCurrent}, OUTP={outputResponse}, " +
+ $"寤惰繜={stopwatch.Elapsed.TotalMilliseconds:F2}ms");
+
+ return new PowerOperationResult
+ {
+ Success = true,
+ MeasuredVoltage = measuredVoltage,
+ MeasuredCurrent = measuredCurrent,
+ RawResponse = rawResponse,
+ LatencyMs = stopwatch.Elapsed.TotalMilliseconds
+ };
+ }
+ catch (ResourceConflictException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 璧勬簮鍐茬獊: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (OperationCanceledException)
+ {
+ stopwatch.Stop();
+ Log.Warn($"[PowerSupplyService] 鐘舵�佹煡璇㈣鍙栨秷: Slot={slotId}");
+ return PowerOperationResult.Failed("OPERATION_CANCELLED", "鎿嶄綔宸茶鍙栨秷",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (TimeoutException ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 鐘舵�佹煡璇㈣秴鏃�: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("TIMEOUT", $"鐘舵�佹煡璇㈣秴鏃�: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ stopwatch.Stop();
+ Log.Error($"[PowerSupplyService] 鐘舵�佹煡璇㈠紓甯�: Slot={slotId}, 閿欒: {ex.Message}", ex);
+ return PowerOperationResult.Failed("UNKNOWN_ERROR", $"鐘舵�佹煡璇㈠け璐�: {ex.Message}",
+ stopwatch.Elapsed.TotalMilliseconds);
+ }
+ }
+
+ /// <summary>
+ /// 鏍规嵁鐢垫簮閰嶇疆鍛戒护鏋勫缓 SCPI 鍛戒护鍒楄〃
+ /// </summary>
+ /// <param name="command">鐢垫簮閰嶇疆鍛戒护</param>
+ /// <returns>SCPI 鍛戒护瀛楃涓插垪琛�</returns>
+ private static List<string> BuildScpiCommands(PowerCommand command)
+ {
+ var commands = new List<string>();
+
+ if (command.Voltage.HasValue)
+ {
+ commands.Add($"VOLT {command.Voltage.Value:G}");
+ }
+
+ if (command.CurrentLimit.HasValue)
+ {
+ commands.Add($"CURR {command.CurrentLimit.Value:G}");
+ }
+
+ if (command.Output.HasValue)
+ {
+ commands.Add(command.Output.Value ? "OUTP ON" : "OUTP OFF");
+ }
+
+ return commands;
+ }
+
+ /// <summary>
+ /// 瑙f瀽璁惧杩斿洖鐨勬祴閲忓�煎搷搴斿瓧绗︿覆銆�
+ /// 鏀寔鏍煎紡: "voltage,current" 鎴栧崟涓暟鍊笺��
+ /// </summary>
+ /// <param name="rawResponse">鍘熷鍝嶅簲瀛楃涓�</param>
+ /// <param name="voltage">瑙f瀽鍚庣殑鐢靛帇鍊�</param>
+ /// <param name="current">瑙f瀽鍚庣殑鐢垫祦鍊�</param>
+ private static void ParseMeasurementResponse(string rawResponse, out double? voltage, out double? current)
+ {
+ voltage = null;
+ current = null;
+
+ if (string.IsNullOrWhiteSpace(rawResponse))
+ return;
+
+ try
+ {
+ var parts = rawResponse.Split(',');
+
+ if (parts.Length >= 1 && double.TryParse(parts[0].Trim(), out var v))
+ {
+ voltage = v;
+ }
+
+ if (parts.Length >= 2 && double.TryParse(parts[1].Trim(), out var c))
+ {
+ current = c;
+ }
+ }
+ catch (Exception)
+ {
+ // 瑙f瀽澶辫触鏃朵繚鎸� null 鍊�
+ }
+ }
+ }
+}
diff --git a/Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs b/Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs
new file mode 100644
index 0000000..15b34cc
--- /dev/null
+++ b/Aging.Infrastructure/Services/SlotDeviceMappingResolver.cs
@@ -0,0 +1,218 @@
+using System.Data;
+using Aging.Infrastructure.Contracts;
+using HMI.Core.Util;
+using log4net;
+
+namespace Aging.Infrastructure.Services
+{
+ /// <summary>
+ /// 鐢垫簮绔偣淇℃伅锛屾弿杩� Slot 鏄犲皠鍒扮殑鍏蜂綋璁惧閫氶亾
+ /// </summary>
+ public sealed class PowerEndpoint
+ {
+ /// <summary>
+ /// 璁惧鏍囪瘑绗�
+ /// </summary>
+ public string DeviceId { get; }
+
+ /// <summary>
+ /// 閫氶亾鏍囪瘑
+ /// </summary>
+ public int ChannelId { get; }
+
+ /// <summary>
+ /// 鐢熸晥鐗堟湰鍙�
+ /// </summary>
+ public string EffectiveVersion { get; }
+
+ /// <summary>
+ /// 鍒濆鍖栫數婧愮鐐瑰疄渚�
+ /// </summary>
+ /// <param name="deviceId">璁惧鏍囪瘑绗�</param>
+ /// <param name="channelId">閫氶亾鏍囪瘑</param>
+ /// <param name="effectiveVersion">鐢熸晥鐗堟湰鍙�</param>
+ public PowerEndpoint(string deviceId, int channelId, string effectiveVersion)
+ {
+ DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId));
+ ChannelId = channelId;
+ EffectiveVersion = effectiveVersion ?? string.Empty;
+ }
+
+ /// <summary>
+ /// 杞崲涓鸿祫婧愰敭锛岀敤浜庤祫婧愮鐞嗗櫒鐨勯绾﹀拰绉熺害鎿嶄綔
+ /// </summary>
+ /// <returns>瀵瑰簲鐨勮祫婧愰敭瀹炰緥</returns>
+ public ResourceKey ToResourceKey()
+ {
+ return new ResourceKey(DeviceId, "PowerSupply", ChannelId.ToString());
+ }
+
+ /// <inheritdoc/>
+ public override string ToString() => $"{DeviceId}:CH{ChannelId}(v{EffectiveVersion})";
+ }
+
+ /// <summary>
+ /// Slot-璁惧-閫氶亾鏄犲皠瑙f瀽鍣ㄣ��
+ /// 浠� Plugin.DeviceConfig 鐨� SQLite 鏁版嵁搴撹鍙� Slot-Device-Channel 鏄犲皠鍏崇郴锛�
+ /// 鎻愪緵鏍规嵁 SlotId 鍜岃兘鍔涚被鍨嬭В鏋愬叿浣撹澶囩鐐圭殑鍔熻兘銆�
+ /// </summary>
+ public sealed class SlotDeviceMappingResolver
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(SlotDeviceMappingResolver));
+
+ /// <summary>
+ /// DeviceConfig 鐨� SQLite 鏁版嵁搴撴枃浠惰矾寰�
+ /// </summary>
+ private readonly string _deviceConfigDbPath;
+
+ /// <summary>
+ /// 鍒濆鍖栨槧灏勮В鏋愬櫒瀹炰緥
+ /// </summary>
+ /// <param name="deviceConfigDbPath">DeviceConfig 鐨� SQLite 鏁版嵁搴撴枃浠惰矾寰�</param>
+ public SlotDeviceMappingResolver(string deviceConfigDbPath)
+ {
+ _deviceConfigDbPath = deviceConfigDbPath ?? throw new ArgumentNullException(nameof(deviceConfigDbPath));
+ Log.Info($"[SlotDeviceMappingResolver] 鍒濆鍖栧畬鎴愶紝鏁版嵁搴撹矾寰�: {deviceConfigDbPath}");
+ }
+
+ /// <summary>
+ /// 鏍规嵁 SlotId 鍜岃兘鍔涚被鍨嬭В鏋愬叿浣撶殑鐢垫簮绔偣銆�
+ /// 浠� slot_device_mapping 琛ㄦ煡璇㈠尮閰嶇殑鏈�鏂扮増鏈槧灏勮褰曘��
+ /// </summary>
+ /// <param name="slotId">Slot 鏍囪瘑绗�</param>
+ /// <param name="capability">鑳藉姏绫诲瀷锛堝 "PowerSupply"锛�</param>
+ /// <returns>瑙f瀽鍚庣殑鐢垫簮绔偣锛涙湭鎵惧埌鍖归厤鏄犲皠鏃惰繑鍥� null</returns>
+ public PowerEndpoint? ResolveEndpoint(string slotId, string capability)
+ {
+ if (string.IsNullOrWhiteSpace(slotId))
+ throw new ArgumentException("SlotId 涓嶈兘涓虹┖", nameof(slotId));
+ if (string.IsNullOrWhiteSpace(capability))
+ throw new ArgumentException("鑳藉姏绫诲瀷涓嶈兘涓虹┖", nameof(capability));
+
+ SqliteHelper? sqliteHelper = null;
+ try
+ {
+ sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
+
+ var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
+ $"FROM slot_device_mapping " +
+ $"WHERE slot_id = '{slotId}' AND capability = '{capability}' " +
+ $"ORDER BY mapping_version DESC LIMIT 1";
+
+ var dataTable = sqliteHelper.GetDataTable(sql);
+
+ if (dataTable.Rows.Count == 0)
+ {
+ Log.Warn($"[SlotDeviceMappingResolver] 鏈壘鍒� Slot={slotId}, Capability={capability} 鐨勬槧灏勮褰�");
+ return null;
+ }
+
+ var row = dataTable.Rows[0];
+ var deviceId = row["device_id"]?.ToString() ?? string.Empty;
+ var channelId = Convert.ToInt32(row["channel_id"]);
+ var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
+
+ var endpoint = new PowerEndpoint(deviceId, channelId, effectiveVersion);
+
+ Log.Info($"[SlotDeviceMappingResolver] 瑙f瀽鏄犲皠: Slot={slotId}, Capability={capability} 鈫� {endpoint}");
+ return endpoint;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[SlotDeviceMappingResolver] 瑙f瀽鏄犲皠澶辫触: Slot={slotId}, Capability={capability}, 閿欒: {ex.Message}", ex);
+ throw;
+ }
+ finally
+ {
+ sqliteHelper?.Close();
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇鏁版嵁搴撲腑鎵�鏈夌殑 Slot-璁惧-閫氶亾鏄犲皠璁板綍
+ /// </summary>
+ /// <returns>鎵�鏈夋槧灏勮褰曠殑鍒楄〃</returns>
+ public List<PowerEndpoint> GetAllMappings()
+ {
+ SqliteHelper? sqliteHelper = null;
+ try
+ {
+ sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
+
+ var sql = "SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
+ "FROM slot_device_mapping " +
+ "ORDER BY slot_id, capability, mapping_version DESC";
+
+ var dataTable = sqliteHelper.GetDataTable(sql);
+ var results = new List<PowerEndpoint>();
+
+ foreach (DataRow row in dataTable.Rows)
+ {
+ var deviceId = row["device_id"]?.ToString() ?? string.Empty;
+ var channelId = Convert.ToInt32(row["channel_id"]);
+ var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
+
+ results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion));
+ }
+
+ Log.Info($"[SlotDeviceMappingResolver] 鑾峰彇鍏ㄩ儴鏄犲皠璁板綍锛屽叡 {results.Count} 鏉�");
+ return results;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[SlotDeviceMappingResolver] 鑾峰彇鍏ㄩ儴鏄犲皠璁板綍澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ finally
+ {
+ sqliteHelper?.Close();
+ }
+ }
+
+ /// <summary>
+ /// 鎸夎兘鍔涚被鍨嬬瓫閫夋槧灏勮褰�
+ /// </summary>
+ /// <param name="capability">鑳藉姏绫诲瀷锛堝 "PowerSupply"銆�"CAN" 绛夛級</param>
+ /// <returns>鍖归厤鎸囧畾鑳藉姏绫诲瀷鐨勬槧灏勮褰曞垪琛�</returns>
+ public List<PowerEndpoint> GetMappingsByCapability(string capability)
+ {
+ if (string.IsNullOrWhiteSpace(capability))
+ throw new ArgumentException("鑳藉姏绫诲瀷涓嶈兘涓虹┖", nameof(capability));
+
+ SqliteHelper? sqliteHelper = null;
+ try
+ {
+ sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
+
+ var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " +
+ $"FROM slot_device_mapping " +
+ $"WHERE capability = '{capability}' " +
+ $"ORDER BY slot_id, mapping_version DESC";
+
+ var dataTable = sqliteHelper.GetDataTable(sql);
+ var results = new List<PowerEndpoint>();
+
+ foreach (DataRow row in dataTable.Rows)
+ {
+ var deviceId = row["device_id"]?.ToString() ?? string.Empty;
+ var channelId = Convert.ToInt32(row["channel_id"]);
+ var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty;
+
+ results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion));
+ }
+
+ Log.Info($"[SlotDeviceMappingResolver] 鎸夎兘鍔涚被鍨� {capability} 绛涢�夋槧灏勮褰曪紝鍏� {results.Count} 鏉�");
+ return results;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[SlotDeviceMappingResolver] 鎸夎兘鍔涚被鍨嬬瓫閫夋槧灏勮褰曞け璐�: {ex.Message}", ex);
+ throw;
+ }
+ finally
+ {
+ sqliteHelper?.Close();
+ }
+ }
+ }
+}
diff --git a/CanSimulator/CanBusSimulator.cs b/CanSimulator/CanBusSimulator.cs
new file mode 100644
index 0000000..3497aac
--- /dev/null
+++ b/CanSimulator/CanBusSimulator.cs
@@ -0,0 +1,403 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using log4net;
+
+namespace CanSimulator
+{
+ /// <summary>
+ /// CAN 鎬荤嚎鐘舵��
+ /// </summary>
+ public enum CanBusState
+ {
+ /// <summary>鍋滄鐘舵��</summary>
+ Stopped,
+ /// <summary>杩愯涓�</summary>
+ Running,
+ /// <summary>鏆傚仠</summary>
+ Paused,
+ /// <summary>閿欒</summary>
+ Error
+ }
+
+ /// <summary>
+ /// CAN 鎬荤嚎妯℃嫙鍣ㄤ簨浠跺弬鏁�
+ /// </summary>
+ public sealed class CanBusFrameEventArgs : EventArgs
+ {
+ public CanFrame Frame { get; }
+ public string SourceNodeId { get; }
+
+ public CanBusFrameEventArgs(CanFrame frame, string sourceNodeId)
+ {
+ Frame = frame;
+ SourceNodeId = sourceNodeId;
+ }
+ }
+
+ /// <summary>
+ /// CAN 鎬荤嚎妯℃嫙鍣�
+ /// 妯℃嫙涓�鏉¤櫄鎷� CAN 鎬荤嚎锛屽涓櫄鎷熻妭鐐瑰彲浠ユ寕杞藉埌鎬荤嚎涓婅繘琛岄�氳
+ /// </summary>
+ public sealed class CanBusSimulator : IDisposable
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(CanBusSimulator));
+
+ /// <summary>
+ /// 鎬荤嚎鍚嶇О
+ /// </summary>
+ public string BusName { get; }
+
+ /// <summary>
+ /// 鎬荤嚎鐘舵��
+ /// </summary>
+ public CanBusState State { get; private set; } = CanBusState.Stopped;
+
+ /// <summary>
+ /// 娉㈢壒鐜囷紙浠呯敤浜庢ā鎷熸樉绀猴紝涓嶅奖鍝嶅疄闄呴�昏緫锛�
+ /// </summary>
+ public int BaudRate { get; set; } = 500000; // 榛樿 500kbps
+
+ /// <summary>
+ /// 妯℃嫙寤惰繜锛堟绉掞級- 妯℃嫙鐪熷疄鎬荤嚎浼犺緭寤惰繜
+ /// </summary>
+ public int SimulatedLatencyMs { get; set; } = 0;
+
+ /// <summary>
+ /// 宸叉敞鍐岀殑鑺傜偣
+ /// </summary>
+ private readonly ConcurrentDictionary<string, CanNode> _nodes = new();
+
+ /// <summary>
+ /// 鎬荤嚎鏃ュ織锛堟渶杩� N 鏉″抚锛�
+ /// </summary>
+ private readonly ConcurrentQueue<CanFrame> _busLog = new();
+ private int _maxLogSize = 5000;
+
+ /// <summary>
+ /// 鎬荤嚎缁熻
+ /// </summary>
+ public long TotalFramesTransmitted { get; private set; }
+ public long TotalFramesReceived { get; private set; }
+ public DateTime? LastActivityTime { get; private set; }
+
+ /// <summary>
+ /// 甯у湪鎬荤嚎涓婁紶杈撲簨浠讹紙鍙敤浜庣洃鎺�/鏃ュ織锛�
+ /// </summary>
+ public event EventHandler<CanBusFrameEventArgs>? FrameTransmitted;
+
+ /// <summary>
+ /// 鎬荤嚎鐘舵�佸彉鍖栦簨浠�
+ /// </summary>
+ public event EventHandler<CanBusState>? StateChanged;
+
+ public CanBusSimulator(string busName = "CAN_BUS_1")
+ {
+ BusName = busName ?? throw new ArgumentNullException(nameof(busName));
+ }
+
+ /// <summary>
+ /// 鍚姩鎬荤嚎
+ /// </summary>
+ public void Start()
+ {
+ if (State == CanBusState.Running)
+ {
+ Log.Warn($"[CanBus] 鎬荤嚎 {BusName} 宸插湪杩愯涓�");
+ return;
+ }
+
+ State = CanBusState.Running;
+ Log.Info($"[CanBus] 鎬荤嚎 {BusName} 宸插惎鍔� (娉㈢壒鐜�: {BaudRate}bps)");
+ StateChanged?.Invoke(this, State);
+ }
+
+ /// <summary>
+ /// 鍋滄鎬荤嚎
+ /// </summary>
+ public void Stop()
+ {
+ if (State == CanBusState.Stopped) return;
+
+ State = CanBusState.Stopped;
+ Log.Info($"[CanBus] 鎬荤嚎 {BusName} 宸插仠姝�");
+ StateChanged?.Invoke(this, State);
+ }
+
+ /// <summary>
+ /// 鏆傚仠鎬荤嚎锛堝抚浠嶅湪淇濈暀浣嗕笉璺敱锛�
+ /// </summary>
+ public void Pause()
+ {
+ if (State != CanBusState.Running) return;
+
+ State = CanBusState.Paused;
+ Log.Info($"[CanBus] 鎬荤嚎 {BusName} 宸叉殏鍋�");
+ StateChanged?.Invoke(this, State);
+ }
+
+ /// <summary>
+ /// 鎭㈠鎬荤嚎
+ /// </summary>
+ public void Resume()
+ {
+ if (State != CanBusState.Paused) return;
+
+ State = CanBusState.Running;
+ Log.Info($"[CanBus] 鎬荤嚎 {BusName} 宸叉仮澶�");
+ StateChanged?.Invoke(this, State);
+ }
+
+ /// <summary>
+ /// 娉ㄥ唽鑺傜偣鍒版�荤嚎
+ /// </summary>
+ public CanNode RegisterNode(string nodeId, string? name = null)
+ {
+ if (string.IsNullOrWhiteSpace(nodeId))
+ throw new ArgumentException("鑺傜偣 ID 涓嶈兘涓虹┖", nameof(nodeId));
+
+ var node = new CanNode(nodeId, name);
+ node.FrameSent += OnNodeFrameSent;
+
+ if (!_nodes.TryAdd(nodeId, node))
+ {
+ Log.Warn($"[CanBus] 鑺傜偣 {nodeId} 宸插瓨鍦紝灏嗚鏇挎崲");
+ _nodes.TryRemove(nodeId, out var oldNode);
+ oldNode?.Deactivate();
+ _nodes.TryAdd(nodeId, node);
+ }
+
+ // 濡傛灉鎬荤嚎宸茶繍琛岋紝鑷姩婵�娲昏妭鐐�
+ if (State == CanBusState.Running)
+ {
+ node.Activate();
+ }
+
+ Log.Info($"[CanBus] 鑺傜偣 {nodeId} ({name ?? nodeId}) 宸叉敞鍐屽埌鎬荤嚎 {BusName}");
+ return node;
+ }
+
+ /// <summary>
+ /// 娉ㄩ攢鑺傜偣
+ /// </summary>
+ public bool UnregisterNode(string nodeId)
+ {
+ if (_nodes.TryRemove(nodeId, out var node))
+ {
+ node.FrameSent -= OnNodeFrameSent;
+ node.Deactivate();
+ Log.Info($"[CanBus] 鑺傜偣 {nodeId} 宸蹭粠鎬荤嚎 {BusName} 娉ㄩ攢");
+ return true;
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// 鑾峰彇鑺傜偣
+ /// </summary>
+ public CanNode? GetNode(string nodeId)
+ {
+ _nodes.TryGetValue(nodeId, out var node);
+ return node;
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈夊凡娉ㄥ唽鑺傜偣
+ /// </summary>
+ public IReadOnlyList<CanNode> GetAllNodes()
+ {
+ return _nodes.Values.ToList();
+ }
+
+ /// <summary>
+ /// 鍚戞�荤嚎鍙戦�佸抚锛堢敱鑺傜偣鍙戦�侊紝鎬荤嚎璐熻矗璺敱锛�
+ /// </summary>
+ private void OnNodeFrameSent(object? sender, CanFrame frame)
+ {
+ if (State != CanBusState.Running) return;
+
+ // 妯℃嫙寤惰繜
+ if (SimulatedLatencyMs > 0)
+ {
+ Thread.Sleep(SimulatedLatencyMs);
+ }
+
+ // 璁板綍鍒版�荤嚎鏃ュ織
+ while (_busLog.Count >= _maxLogSize)
+ {
+ _busLog.TryDequeue(out _);
+ }
+ _busLog.Enqueue(frame);
+ TotalFramesTransmitted++;
+ LastActivityTime = DateTime.Now;
+
+ // 璺敱鍒版墍鏈夊叾浠栬妭鐐�
+ int routed = 0;
+ foreach (var kvp in _nodes)
+ {
+ if (kvp.Key == frame.SourceNodeId) continue; // 涓嶈矾鐢卞洖婧愯妭鐐�
+
+ try
+ {
+ kvp.Value.ReceiveFrame(frame);
+ routed++;
+ TotalFramesReceived++;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[CanBus] 璺敱甯у埌鑺傜偣 {kvp.Key} 澶辫触: {ex.Message}");
+ }
+ }
+
+ // 妫�鏌ユ簮鑺傜偣鏄惁鍚敤鑷姩鍝嶅簲妯″紡
+ if (_nodes.TryGetValue(frame.SourceNodeId, out var sourceNode) && sourceNode.AutoResponseEnabled)
+ {
+ try
+ {
+ // 妯℃嫙鍝嶅簲寤惰繜
+ if (sourceNode.AutoResponseDelayMs > 0)
+ {
+ Thread.Sleep(sourceNode.AutoResponseDelayMs);
+ }
+
+ // 鍒涘缓鍝嶅簲甯э紙澶嶅埗鍘熷甯ф暟鎹紝鏉ユ簮涓烘簮鑺傜偣锛�
+ var responseFrame = new CanFrame
+ {
+ FrameId = frame.FrameId,
+ Format = frame.Format,
+ Type = frame.Type,
+ Data = (byte[])frame.Data.Clone(),
+ SourceNodeId = sourceNode.NodeId,
+ Timestamp = DateTime.Now
+ };
+
+ // 灏嗗搷搴斿抚璺敱鍒版墍鏈夊叾浠栬妭鐐癸紙涓嶅寘鎷搷搴旀簮鑷韩锛�
+ foreach (var kvp in _nodes)
+ {
+ if (kvp.Key == sourceNode.NodeId) continue;
+ try
+ {
+ kvp.Value.ReceiveFrame(responseFrame);
+ TotalFramesReceived++;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[CanBus] 璺敱鍝嶅簲甯у埌鑺傜偣 {kvp.Key} 澶辫触: {ex.Message}");
+ }
+ }
+
+ // 灏嗗搷搴斿抚閫佸叆婧愯妭鐐圭殑鎺ユ敹閭锛堣婧愯妭鐐硅兘鐪嬪埌鑷繁鐨勫搷搴旓級
+ sourceNode.ReceiveFrame(responseFrame);
+ TotalFramesReceived++;
+
+ // 璁板綍鍝嶅簲甯у埌鎬荤嚎鏃ュ織
+ _busLog.Enqueue(responseFrame);
+ TotalFramesTransmitted++;
+
+ Log.Debug($"[CanBus] 鑺傜偣 {sourceNode.NodeId} 鑷姩鍝嶅簲甯� {responseFrame.FrameIdHex}");
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[CanBus] 婧愯妭鐐硅嚜鍔ㄥ搷搴斿け璐�: {ex.Message}");
+ }
+ }
+
+ Log.Debug($"[CanBus] 甯� {frame.FrameIdHex} 浠� {frame.SourceNodeId} 璺敱鍒� {routed} 涓妭鐐�");
+ FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId));
+ }
+
+ /// <summary>
+ /// 鐩存帴娉ㄥ叆甯у埌鎬荤嚎锛堢敤浜庢ā鎷熷閮ㄨ澶囧彂閫侊級
+ /// </summary>
+ public void InjectFrame(CanFrame frame)
+ {
+ if (State != CanBusState.Running)
+ {
+ Log.Warn($"[CanBus] 鎬荤嚎鏈繍琛岋紝鏃犳硶娉ㄥ叆甯�");
+ return;
+ }
+
+ frame.Timestamp = DateTime.Now;
+
+ // 璁板綍鍒版棩蹇�
+ while (_busLog.Count >= _maxLogSize)
+ {
+ _busLog.TryDequeue(out _);
+ }
+ _busLog.Enqueue(frame);
+ TotalFramesTransmitted++;
+ LastActivityTime = DateTime.Now;
+
+ // 璺敱鍒版墍鏈夎妭鐐�
+ foreach (var kvp in _nodes)
+ {
+ try
+ {
+ kvp.Value.ReceiveFrame(frame);
+ TotalFramesReceived++;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"[CanBus] 娉ㄥ叆甯ц矾鐢卞埌鑺傜偣 {kvp.Key} 澶辫触: {ex.Message}");
+ }
+ }
+
+ FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId));
+ Log.Debug($"[CanBus] 娉ㄥ叆甯� {frame.FrameIdHex} 鍒版�荤嚎");
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎬荤嚎鏃ュ織
+ /// </summary>
+ public List<CanFrame> GetBusLog(int maxCount = 100)
+ {
+ var all = _busLog.ToList();
+ if (all.Count <= maxCount) return all;
+ return all.Skip(all.Count - maxCount).ToList();
+ }
+
+ /// <summary>
+ /// 娓呯┖鎬荤嚎鏃ュ織
+ /// </summary>
+ public void ClearBusLog()
+ {
+ while (_busLog.TryDequeue(out _)) { }
+ }
+
+ /// <summary>
+ /// 閲嶇疆鎵�鏈夌粺璁�
+ /// </summary>
+ public void ResetStatistics()
+ {
+ TotalFramesTransmitted = 0;
+ TotalFramesReceived = 0;
+ LastActivityTime = null;
+ foreach (var node in _nodes.Values)
+ {
+ node.ResetCounters();
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎬荤嚎鐘舵�佹憳瑕�
+ /// </summary>
+ public string GetStatusSummary()
+ {
+ return $"Bus={BusName}, State={State}, BaudRate={BaudRate}, Nodes={_nodes.Count}, " +
+ $"Tx={TotalFramesTransmitted}, Rx={TotalFramesReceived}, " +
+ $"LastActivity={LastActivityTime?.ToString("HH:mm:ss") ?? "N/A"}";
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ foreach (var node in _nodes.Values)
+ {
+ node.Deactivate();
+ }
+ _nodes.Clear();
+ }
+ }
+}
diff --git a/CanSimulator/CanFrame.cs b/CanSimulator/CanFrame.cs
new file mode 100644
index 0000000..c70b60c
--- /dev/null
+++ b/CanSimulator/CanFrame.cs
@@ -0,0 +1,130 @@
+using System;
+
+namespace CanSimulator
+{
+ /// <summary>
+ /// CAN 甯ф牸寮忕被鍨�
+ /// </summary>
+ public enum CanFrameFormat
+ {
+ /// <summary>鏍囧噯甯� (11-bit ID)</summary>
+ Standard,
+ /// <summary>鎵╁睍甯� (29-bit ID)</summary>
+ Extended
+ }
+
+ /// <summary>
+ /// CAN 甯х被鍨�
+ /// </summary>
+ public enum CanFrameType
+ {
+ /// <summary>鏁版嵁甯�</summary>
+ Data,
+ /// <summary>杩滅▼甯�</summary>
+ Remote
+ }
+
+ /// <summary>
+ /// CAN 鎬荤嚎甯ф暟鎹粨鏋�
+ /// </summary>
+ public sealed class CanFrame
+ {
+ /// <summary>
+ /// 甯� ID锛堟爣鍑嗗抚 11-bit锛屾墿灞曞抚 29-bit锛�
+ /// </summary>
+ public uint FrameId { get; set; }
+
+ /// <summary>
+ /// 甯ф牸寮忥紙鏍囧噯/鎵╁睍锛�
+ /// </summary>
+ public CanFrameFormat Format { get; set; } = CanFrameFormat.Standard;
+
+ /// <summary>
+ /// 甯х被鍨嬶紙鏁版嵁甯�/杩滅▼甯э級
+ /// </summary>
+ public CanFrameType Type { get; set; } = CanFrameType.Data;
+
+ /// <summary>
+ /// 鏁版嵁璐熻浇锛堟渶澶� 8 瀛楄妭锛孋AN FD 鍙揪 64 瀛楄妭锛�
+ /// </summary>
+ public byte[] Data { get; set; } = Array.Empty<byte>();
+
+ /// <summary>
+ /// 鏃堕棿鎴�
+ /// </summary>
+ public DateTime Timestamp { get; set; } = DateTime.Now;
+
+ /// <summary>
+ /// 鍙戦�佽妭鐐规爣璇�
+ /// </summary>
+ public string SourceNodeId { get; set; } = string.Empty;
+
+ /// <summary>
+ /// DLC (Data Length Code)
+ /// </summary>
+ public int Dlc => Data.Length;
+
+ /// <summary>
+ /// 灏嗗抚 ID 鏍煎紡鍖栦负鍗佸叚杩涘埗瀛楃涓�
+ /// </summary>
+ public string FrameIdHex => Format == CanFrameFormat.Extended
+ ? $"0x{FrameId:X8}"
+ : $"0x{FrameId:X3}";
+
+ /// <summary>
+ /// 灏嗘暟鎹牸寮忓寲涓哄崄鍏繘鍒跺瓧绗︿覆
+ /// </summary>
+ public string DataHex => BitConverter.ToString(Data).Replace("-", " ");
+
+ /// <summary>
+ /// 浠庡崄鍏繘鍒跺瓧绗︿覆瑙f瀽甯� ID
+ /// </summary>
+ public static uint ParseFrameId(string frameIdStr)
+ {
+ if (string.IsNullOrWhiteSpace(frameIdStr))
+ throw new ArgumentException("甯� ID 涓嶈兘涓虹┖", nameof(frameIdStr));
+
+ if (frameIdStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ if (!uint.TryParse(frameIdStr.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out uint id))
+ throw new ArgumentException($"鏃犳硶瑙f瀽甯� ID: {frameIdStr}");
+ return id;
+ }
+ else
+ {
+ if (!uint.TryParse(frameIdStr, out uint id))
+ throw new ArgumentException($"鏃犳硶瑙f瀽甯� ID: {frameIdStr}");
+ return id;
+ }
+ }
+
+ /// <summary>
+ /// 浠庡崄鍏繘鍒跺瓧绗︿覆瑙f瀽鏁版嵁
+ /// </summary>
+ public static byte[] ParseData(string dataStr)
+ {
+ if (string.IsNullOrWhiteSpace(dataStr))
+ return Array.Empty<byte>();
+
+ try
+ {
+ var parts = dataStr.Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries);
+ var bytes = new byte[parts.Length];
+ for (int i = 0; i < parts.Length; i++)
+ {
+ bytes[i] = byte.Parse(parts[i], System.Globalization.NumberStyles.HexNumber);
+ }
+ return bytes;
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException($"鏃犳硶瑙f瀽鏁版嵁瀛楃涓�: {dataStr}, 閿欒: {ex.Message}");
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"[{Timestamp:HH:mm:ss.fff}] {FrameIdHex} [{Dlc}] {DataHex} (from {SourceNodeId})";
+ }
+ }
+}
diff --git a/CanSimulator/CanNode.cs b/CanSimulator/CanNode.cs
new file mode 100644
index 0000000..361583b
--- /dev/null
+++ b/CanSimulator/CanNode.cs
@@ -0,0 +1,311 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using log4net;
+
+namespace CanSimulator
+{
+ /// <summary>
+ /// 鎺ユ敹婊ゆ尝鍣ㄩ厤缃�
+ /// </summary>
+ public sealed class AcceptanceFilter
+ {
+ /// <summary>
+ /// 婊ゆ尝鍣� ID锛堝尮閰嶅�硷級
+ /// </summary>
+ public uint FilterId { get; set; }
+
+ /// <summary>
+ /// 鎺╃爜锛堜负 1 鐨勪綅鍙備笌姣旇緝锛�
+ /// </summary>
+ public uint Mask { get; set; } = 0x7FF; // 榛樿鏍囧噯甯у叏鍖归厤
+
+ /// <summary>
+ /// 鏄惁涓烘墿灞曞抚婊ゆ尝鍣�
+ /// </summary>
+ public bool IsExtended { get; set; }
+
+ /// <summary>
+ /// 妫�鏌ョ粰瀹氬抚 ID 鏄惁閫氳繃姝ゆ护娉㈠櫒
+ /// </summary>
+ public bool Accepts(uint frameId, bool isExtended)
+ {
+ if (IsExtended != isExtended) return false;
+ return (frameId & Mask) == (FilterId & Mask);
+ }
+ }
+
+ /// <summary>
+ /// 铏氭嫙 CAN 鑺傜偣
+ /// 妯℃嫙涓�涓� CAN 閫氳鍗$殑绔偣锛屽叿鏈夊彂閫佸拰鎺ユ敹鍔熻兘
+ /// </summary>
+ public sealed class CanNode
+ {
+ private static readonly ILog Log = LogManager.GetLogger(typeof(CanNode));
+
+ /// <summary>
+ /// 鑺傜偣鍞竴鏍囪瘑
+ /// </summary>
+ public string NodeId { get; }
+
+ /// <summary>
+ /// 鑺傜偣鍚嶇О锛堝彲閫夋弿杩帮級
+ /// </summary>
+ public string Name { get; set; }
+
+ /// <summary>
+ /// 鑺傜偣鏄惁宸叉縺娲伙紙杩炴帴鍒版�荤嚎锛�
+ /// </summary>
+ public bool IsActive { get; private set; }
+
+ /// <summary>
+ /// 鎺ユ敹婊ゆ尝鍣ㄥ垪琛�
+ /// </summary>
+ private readonly List<AcceptanceFilter> _filters = new();
+
+ /// <summary>
+ /// 鎺ユ敹閭锛團IFO 闃熷垪锛�
+ /// </summary>
+ private readonly ConcurrentQueue<CanFrame> _receiveMailbox = new();
+
+ /// <summary>
+ /// 鍙戦�佸巻鍙茶褰�
+ /// </summary>
+ private readonly ConcurrentQueue<CanFrame> _sendHistory = new();
+
+ /// <summary>
+ /// 鏈�澶ч偖绠卞閲�
+ /// </summary>
+ public int MaxMailboxSize { get; set; } = 1000;
+
+ /// <summary>
+ /// 鏈�澶у巻鍙茶褰曞閲�
+ /// </summary>
+ public int MaxHistorySize { get; set; } = 500;
+
+ /// <summary>
+ /// 鎺ユ敹甯ц鏁板櫒
+ /// </summary>
+ public long RxCount { get; private set; }
+
+ /// <summary>
+ /// 鍙戦�佸抚璁℃暟鍣�
+ /// </summary>
+ public long TxCount { get; private set; }
+
+ /// <summary>
+ /// 閿欒璁℃暟鍣�
+ /// </summary>
+ public long ErrorCount { get; private set; }
+
+ /// <summary>
+ /// 鏄惁鍚敤鑷姩鍝嶅簲妯″紡锛堟敹鍒板抚鍚庡師灏佷笉鍔ㄥ洖澶嶏級
+ /// </summary>
+ public bool AutoResponseEnabled { get; set; }
+
+ /// <summary>
+ /// 鑷姩鍝嶅簲寤惰繜锛堟绉掞級- 妯℃嫙鐪熷疄璁惧鍝嶅簲鏃堕棿
+ /// </summary>
+ public int AutoResponseDelayMs { get; set; } = 0;
+
+ /// <summary>
+ /// 鑷姩鍝嶅簲浜嬩欢锛堢敤浜庢棩蹇�/鐩戞帶锛�
+ /// </summary>
+ public event EventHandler<CanFrame>? AutoResponseSent;
+
+ /// <summary>
+ /// 甯ф帴鏀朵簨浠�
+ /// </summary>
+ public event EventHandler<CanFrame>? FrameReceived;
+
+ /// <summary>
+ /// 甯у彂閫佷簨浠�
+ /// </summary>
+ public event EventHandler<CanFrame>? FrameSent;
+
+ public CanNode(string nodeId, string? name = null)
+ {
+ NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId));
+ Name = name ?? nodeId;
+ }
+
+ /// <summary>
+ /// 婵�娲昏妭鐐癸紙杩炴帴鍒版�荤嚎锛�
+ /// </summary>
+ public void Activate()
+ {
+ IsActive = true;
+ Log.Info($"[CanNode] 鑺傜偣 {NodeId} ({Name}) 宸叉縺娲�");
+ }
+
+ /// <summary>
+ /// 鍋滅敤鑺傜偣锛堜粠鎬荤嚎鏂紑锛�
+ /// </summary>
+ public void Deactivate()
+ {
+ IsActive = false;
+ Log.Info($"[CanNode] 鑺傜偣 {NodeId} ({Name}) 宸插仠鐢�");
+ }
+
+ /// <summary>
+ /// 娣诲姞鎺ユ敹婊ゆ尝鍣�
+ /// </summary>
+ public void AddFilter(AcceptanceFilter filter)
+ {
+ lock (_filters)
+ {
+ _filters.Add(filter);
+ }
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 娣诲姞婊ゆ尝鍣�: ID=0x{filter.FilterId:X3}, Mask=0x{filter.Mask:X3}");
+ }
+
+ /// <summary>
+ /// 娓呴櫎鎵�鏈夋护娉㈠櫒
+ /// </summary>
+ public void ClearFilters()
+ {
+ lock (_filters)
+ {
+ _filters.Clear();
+ }
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 娓呴櫎鎵�鏈夋护娉㈠櫒");
+ }
+
+ /// <summary>
+ /// 璁剧疆閫氶厤绗︽护娉㈠櫒锛堟帴鏀舵墍鏈夊抚锛�
+ /// </summary>
+ public void SetPromiscuousMode()
+ {
+ lock (_filters)
+ {
+ _filters.Clear();
+ _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = false });
+ _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = true });
+ }
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 璁剧疆涓烘贩鏉傛ā寮忥紙鎺ユ敹鎵�鏈夊抚锛�");
+ }
+
+ /// <summary>
+ /// 妫�鏌ュ抚鏄惁閫氳繃婊ゆ尝鍣�
+ /// </summary>
+ internal bool AcceptsFrame(CanFrame frame)
+ {
+ lock (_filters)
+ {
+ // 濡傛灉娌℃湁璁剧疆婊ゆ尝鍣紝榛樿鎷掔粷
+ if (_filters.Count == 0) return false;
+
+ // 浠讳竴婊ゆ尝鍣ㄥ尮閰嶅嵆鎺ュ彈
+ return _filters.Any(f => f.Accepts(frame.FrameId, frame.Format == CanFrameFormat.Extended));
+ }
+ }
+
+ /// <summary>
+ /// 鎺ユ敹甯э紙鐢辨�荤嚎璋冪敤锛�
+ /// </summary>
+ internal void ReceiveFrame(CanFrame frame)
+ {
+ if (!IsActive) return;
+ if (!AcceptsFrame(frame)) return;
+
+ // 鍏ラ槦鎺ユ敹閭
+ while (_receiveMailbox.Count >= MaxMailboxSize)
+ {
+ _receiveMailbox.TryDequeue(out _); // 涓㈠純鏈�鏃х殑甯�
+ }
+ _receiveMailbox.Enqueue(frame);
+ RxCount++;
+
+ // 瑙﹀彂浜嬩欢
+ FrameReceived?.Invoke(this, frame);
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 鎺ユ敹甯�: {frame}");
+ }
+
+ /// <summary>
+ /// 鍙戦�佸抚
+ /// </summary>
+ public void SendFrame(CanFrame frame)
+ {
+ if (!IsActive)
+ {
+ ErrorCount++;
+ throw new InvalidOperationException($"鑺傜偣 {NodeId} 鏈縺娲伙紝鏃犳硶鍙戦�佸抚");
+ }
+
+ frame.SourceNodeId = NodeId;
+ frame.Timestamp = DateTime.Now;
+
+ // 璁板綍鍙戦�佸巻鍙�
+ while (_sendHistory.Count >= MaxHistorySize)
+ {
+ _sendHistory.TryDequeue(out _);
+ }
+ _sendHistory.Enqueue(frame);
+ TxCount++;
+
+ // 瑙﹀彂浜嬩欢锛堢敱鎬荤嚎澶勭悊璺敱锛�
+ FrameSent?.Invoke(this, frame);
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 鍙戦�佸抚: {frame}");
+ }
+
+ /// <summary>
+ /// 浠庢帴鏀堕偖绠变腑鍙栧嚭涓�甯�
+ /// </summary>
+ public bool TryDequeueReceived(out CanFrame? frame)
+ {
+ return _receiveMailbox.TryDequeue(out frame);
+ }
+
+ /// <summary>
+ /// 鏌ョ湅鎺ユ敹閭涓殑鎵�鏈夊抚锛堜笉绉婚櫎锛�
+ /// </summary>
+ public List<CanFrame> PeekReceiveMailbox()
+ {
+ return _receiveMailbox.ToList();
+ }
+
+ /// <summary>
+ /// 娓呯┖鎺ユ敹閭
+ /// </summary>
+ public void ClearReceiveMailbox()
+ {
+ while (_receiveMailbox.TryDequeue(out _)) { }
+ Log.Debug($"[CanNode] 鑺傜偣 {NodeId} 娓呯┖鎺ユ敹閭");
+ }
+
+ /// <summary>
+ /// 鑾峰彇鍙戦�佸巻鍙�
+ /// </summary>
+ public List<CanFrame> GetSendHistory()
+ {
+ return _sendHistory.ToList();
+ }
+
+ /// <summary>
+ /// 娓呯┖鍙戦�佸巻鍙�
+ /// </summary>
+ public void ClearSendHistory()
+ {
+ while (_sendHistory.TryDequeue(out _)) { }
+ }
+
+ /// <summary>
+ /// 閲嶇疆鑺傜偣缁熻
+ /// </summary>
+ public void ResetCounters()
+ {
+ RxCount = 0;
+ TxCount = 0;
+ ErrorCount = 0;
+ }
+
+ /// <summary>
+ /// 鑾峰彇鑺傜偣鐘舵�佹憳瑕�
+ /// </summary>
+ public string GetStatusSummary()
+ {
+ return $"Node={NodeId}, Active={IsActive}, Rx={RxCount}, Tx={TxCount}, Err={ErrorCount}, Mailbox={_receiveMailbox.Count}";
+ }
+ }
+}
diff --git a/CanSimulator/CanSimulator.csproj b/CanSimulator/CanSimulator.csproj
new file mode 100644
index 0000000..d3cd792
--- /dev/null
+++ b/CanSimulator/CanSimulator.csproj
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="log4net" Version="3.0.4" />
+ </ItemGroup>
+
+</Project>
diff --git a/Config/Database/database.db b/Config/Database/database.db
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Config/Database/database.db
diff --git a/Config/LogViewer/log_2026-10-23.db b/Config/LogViewer/log_2026-10-23.db
new file mode 100644
index 0000000..255e7f1
--- /dev/null
+++ b/Config/LogViewer/log_2026-10-23.db
Binary files differ
diff --git a/HMI.Core/Setting/ParamSetting.cs b/HMI.Core/Setting/ParamSetting.cs
index f14a912..128326b 100644
--- a/HMI.Core/Setting/ParamSetting.cs
+++ b/HMI.Core/Setting/ParamSetting.cs
@@ -65,6 +65,7 @@
public static bool ShowBurnInAbort { get; private set; } = true;
public static string SocketNames { get; private set; } = string.Empty;
public static string StationName { get; private set; } = string.Empty;
+ public static string MainSeqPath { get; private set; } = string.Empty;
public static string EngineType
@@ -225,6 +226,16 @@
{
}
+
+ try
+ {
+
+ MainSeqPath = ParameterManager.GetCommonParameterByName<string>("BurnInMainSeq", string.Empty);
+ }
+ catch
+ {
+
+ }
}
}
}
diff --git a/HMI.Core/Util/LangUtil.cs b/HMI.Core/Util/LangUtil.cs
index ce6f069..bed038e 100644
--- a/HMI.Core/Util/LangUtil.cs
+++ b/HMI.Core/Util/LangUtil.cs
@@ -13,8 +13,9 @@
{
public class LangUtil
{
+ public const string DefaultLang = "zh-CN";
public static List<LangInfo> langs = new List<LangInfo>() {
- new LangInfo("zh-CN","涓枃"),
+ new LangInfo(DefaultLang,"涓枃"),
new LangInfo("en-US","English")
};
@@ -32,7 +33,7 @@
private static string GetLang()
{
var lang = SystemSetting.Instance.CurrentLanguage;
- if (string.IsNullOrEmpty(lang)) lang = CultureInfo.CurrentUICulture.ToString();
+ if (string.IsNullOrEmpty(lang)) lang = "zh-CN";
return lang;
}
@@ -57,10 +58,7 @@
{
try
{
- /// 鎸塉son鍔犺浇
- //LoadAllDataByJson();
- /// 涓嬮潰鏄寜xaml鍔犺浇
- LoadAllDataByXaml();
+ LoadAllData();
LoadLang();
}
finally
@@ -132,56 +130,41 @@
ResourceMap[lang] = resource;
}
-
- private static void LoadDataByXaml(string lang)
+ private static void LoadXamlFromJson2(string lang)
{
- string langFolder = PathHelper.LangFolder;
- string folder = Path.Combine(langFolder, lang);
- if (!Directory.Exists(folder))
+ LoadDataByJson(lang);
+
+ if (!JsonMap.ContainsKey(lang)) return;
+ JObject json = JsonMap[lang];
+ var resource = new ResourceDictionary();
+ foreach (var item in json)
{
- return;
+ resource.Add(item.Key, item.Value.ToString());
}
-
- string[] files = Directory.GetFiles(folder, "*.xaml", SearchOption.AllDirectories);
-
- foreach (var path in files)
+ ResourceMap[lang] = resource;
+ if (lang != DefaultLang)
{
- try
+ var defaultMap = new ResourceDictionary();
+ foreach (var item in json)
{
- using (FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read))
- {
- var resource = (ResourceDictionary)XamlReader.Load(stream);
- if (!ResourceMap.ContainsKey(lang))
- {
- ResourceMap[lang] = resource;
- }
- else
- {
- ResourceMap[lang].MergedDictionaries.Add(resource);
- }
- }
+ defaultMap.Add(item.Key, item.Key);
}
- catch
- {
- }
+ ResourceMap[DefaultLang] = defaultMap;
}
}
- private static void LoadAllDataByJson()
- {
- foreach (var lang in langs)
- {
- LoadDataByJson(lang.Code);
- }
- }
-
- private static void LoadAllDataByXaml()
+ private static void LoadAllData()
{
foreach (var lang in langs)
{
LoadXamlFromJson(lang.Code);
}
}
+
+ public static string Format(string key, params object[] datas)
+ {
+ return string.Format(Get(key), datas);
+ }
}
public static class LangReader
diff --git a/HMI.UI/Resources/Font/iconfont.ttf b/HMI.UI/Resources/Font/iconfont.ttf
index 5ab6b13..55f60da 100644
--- a/HMI.UI/Resources/Font/iconfont.ttf
+++ b/HMI.UI/Resources/Font/iconfont.ttf
Binary files differ
diff --git a/HMI.UI/Style/PanuonResource.xaml b/HMI.UI/Style/PanuonResource.xaml
index bccebeb..315b3d9 100644
--- a/HMI.UI/Style/PanuonResource.xaml
+++ b/HMI.UI/Style/PanuonResource.xaml
@@ -11,7 +11,7 @@
xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI">
<ResourceDictionary.MergedDictionaries>
- <pu:StyleDictionary Includes="PasswordBox,TextBox,ComboBox,Button,TreeView,Menu,ListBox,ContextMenu,TabControl" />
+ <pu:StyleDictionary Includes="PasswordBox,TextBox,ComboBox,Button,TreeView,ListBox" />
<ResourceDictionary Source="/HMI.UI;component/Style/PanuonDataGrid.xaml"/>
<ResourceDictionary Source="/HMI.UI;component/Style/ScrollBarResource.xaml"/>
<ResourceDictionary Source="/HMI.UI;component/Resources/Font/Font.xaml"/>
@@ -146,6 +146,13 @@
</Style>
<Style x:Key="CommonMenu" TargetType="Menu" BasedOn="{StaticResource {x:Static pu:StyleKeys.MenuStyle}}">
+ <Style.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <pu:StyleDictionary Includes="Menu" />
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </Style.Resources>
<Setter Property="Background" Value="{DynamicResource PrimaryColor}"/>
<Setter Property="Foreground" Value="#FFF"/>
<Setter Property="pu:IconHelper.FontFamily" Value="{StaticResource iconfont}"/>
diff --git a/HMI.UnitTest/HMI.UnitTest.csproj b/HMI.UnitTest/HMI.UnitTest.csproj
index 148a599..ee701a0 100644
--- a/HMI.UnitTest/HMI.UnitTest.csproj
+++ b/HMI.UnitTest/HMI.UnitTest.csproj
@@ -55,6 +55,9 @@
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
+ <Reference Include="HslCommunication">
+ <HintPath>..\Libs\HslCommunication.dll</HintPath>
+ </Reference>
<Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
<HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath>
</Reference>
@@ -131,10 +134,6 @@
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\UILib\UILib.csproj">
- <Project>{27325fe0-3ce2-4cd5-8eb5-7e45cc441479}</Project>
- <Name>UILib</Name>
- </ProjectReference>
<ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj">
<Project>{d0806fa6-3f6d-4304-81b8-ead9da7cb5b2}</Project>
<Name>ConnectionUtil</Name>
@@ -159,6 +158,10 @@
<Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project>
<Name>HMI.UI</Name>
</ProjectReference>
+ <ProjectReference Include="..\PLCHelper\PLCHelper.csproj">
+ <Project>{C3F17E03-EF05-4203-8D29-3AD984F3BD9C}</Project>
+ <Name>PLCHelper</Name>
+ </ProjectReference>
<ProjectReference Include="..\Plugin.DataBoard\Plugin.DataBoard.csproj">
<Project>{be42ab26-3270-43e1-84b8-6929d862ff8d}</Project>
<Name>Plugin.DataBoard</Name>
@@ -175,6 +178,10 @@
<Project>{22dc8d91-50b2-47ca-86a8-29a6da48f388}</Project>
<Name>Plugin.UICollection</Name>
</ProjectReference>
+ <ProjectReference Include="..\S7Driver\S7Driver.csproj">
+ <Project>{628ef12b-9ed2-4b20-950f-dc3ecc3fd15f}</Project>
+ <Name>S7Driver</Name>
+ </ProjectReference>
<ProjectReference Include="..\SimpleUtil\HMI.Simple.Util\HMI.Simple.Util.csproj">
<Project>{cf5bebe9-2803-49c3-b773-73d082e0984a}</Project>
<Name>HMI.Simple.Util</Name>
diff --git a/HMI.UnitTest/MainWindow.xaml.cs b/HMI.UnitTest/MainWindow.xaml.cs
index 4dc0549..d0fbc30 100644
--- a/HMI.UnitTest/MainWindow.xaml.cs
+++ b/HMI.UnitTest/MainWindow.xaml.cs
@@ -2,8 +2,11 @@
using HMI.Core.Api;
using HMI.Core.Util;
using HMI.Simple.Util;
+using HslCommunication.Core.Pipe;
+using HslCommunication.Profinet.Siemens;
using log4net;
using Panuon.WPF.UI;
+using PLCHelper;
using Plugin.DataBoard;
using Plugin.TestResult_WPF;
using Plugin.TsExecution;
@@ -11,7 +14,7 @@
using System;
using System.IO;
using System.Windows;
-using Util.ManualDebug;
+using System.Xml.Linq;
namespace UnitTest
{
/// <summary>
@@ -22,16 +25,32 @@
private ILog log = LogManager.GetLogger("UnitTest");
public MainWindow()
{
- ProductModelHelper.GetModelBySNR("cc123", out string model, out string msg);
- LangUtil.Load();
- InitializeComponent();
- //RebuildDb();
- border2.Child = new TcpDebugger();
+ //ProductModelHelper.GetModelBySNR("cc123", out string model, out string msg);
+ //LangUtil.Load();
+ //InitializeComponent();
+ ////RebuildDb();
+ //border2.Child = new TcpDebugger();
-
+ var client = new SiemensS7Net(SiemensPLCS.S1500);
+ PipeTcpNet pipeTcpNet = new PipeTcpNet("127.0.0.1", 102);
+ pipeTcpNet.ConnectTimeOut = 200;
+ pipeTcpNet.ReceiveTimeOut = 200;
+ client.CommunicationPipe = pipeTcpNet;
+ client.ConnectServer();
+
+ var ss = new IntReader(client, "DB10.DBW100", 0);
+ ss.DataChanged += Ss_DataChanged;
+ ss.Run();
+
+
//page.Children.Add(new UserControl1());
+ }
+
+ private void Ss_DataChanged(object sender, DataChangedArg<int> e)
+ {
+ MessageBox.Show($"{e.oldValue} => {e.newValue}");
}
TcpClientWrapper tcpClient;
@@ -128,12 +147,13 @@
private void Button_Click(object sender, RoutedEventArgs e)
{
- var win = new ManualDebug()
- {
- seqPath = "C:\\Users\\chrry\\Desktop\\鎵嬪姩璋冭瘯绀轰緥.seq"
- };
+ //var win = new ManualDebug()
+ //{
+ // seqPath = "C:\\Users\\chrry\\Desktop\\鎵嬪姩璋冭瘯绀轰緥.seq"
+ //};
- win.Show();
+ //win.Show();
+
//Task.Run(() =>
//{
// for (int j = 0; j < 1000; j++)
diff --git a/HMI.Util/IniFileHelper.cs b/HMI.Util/IniFileHelper.cs
index 1c2959b..2494ec7 100644
--- a/HMI.Util/IniFileHelper.cs
+++ b/HMI.Util/IniFileHelper.cs
@@ -89,7 +89,7 @@
return temp.ToString();
}
else
- return String.Empty;
+ return NoText;
}
#endregion
diff --git a/PLCHelper/BoolReader.cs b/PLCHelper/BoolReader.cs
new file mode 100644
index 0000000..ee8635c
--- /dev/null
+++ b/PLCHelper/BoolReader.cs
@@ -0,0 +1,60 @@
+锘縰sing HslCommunication.Profinet.Siemens;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PLCHelper
+{
+
+ public class BoolReader
+ {
+ private bool lastVallue = false;
+
+ private string Address = string.Empty;
+ private int Timeout = 1000;
+
+ private SiemensS7Net client;
+
+ private volatile bool isRunning = false;
+
+ public event EventHandler<DataChangedArg<bool>> DataChanged;
+
+ public BoolReader(SiemensS7Net client, string address, bool initValue = false)
+ {
+ this.client = client;
+ this.Address = address;
+ this.lastVallue = initValue;
+ }
+
+ public void Stop()
+ {
+ isRunning = false;
+ }
+
+ public void Run()
+ {
+ isRunning = true;
+ Task.Run(() =>
+ {
+ while (isRunning)
+ {
+ var res = client.ReadBool(Address);
+ if (res.IsSuccess)
+ {
+ bool val = res.Content;
+ if (val != lastVallue)
+ {
+ DataChanged?.Invoke(this, new DataChangedArg<bool>
+ {
+ oldValue = lastVallue,
+ newValue = val
+ });
+ lastVallue = val;
+ }
+ }
+ Thread.Sleep(Timeout);
+ }
+ });
+ }
+ }
+}
diff --git a/PLCHelper/IntReader.cs b/PLCHelper/IntReader.cs
new file mode 100644
index 0000000..16536cb
--- /dev/null
+++ b/PLCHelper/IntReader.cs
@@ -0,0 +1,65 @@
+锘縰sing HslCommunication.Profinet.Siemens;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PLCHelper
+{
+ public class DataChangedArg<T>
+ {
+ public T oldValue { get; set; }
+ public T newValue { get; set; }
+ }
+
+ public class IntReader
+ {
+ private int lastVallue = 0;
+
+ private string Address = string.Empty;
+ private int Timeout = 1000;
+
+ private SiemensS7Net client;
+
+ private volatile bool isRunning = false;
+
+ public event EventHandler<DataChangedArg<int>> DataChanged;
+
+ public IntReader(SiemensS7Net client, string address, int initValue = 0)
+ {
+ this.client = client;
+ this.Address = address;
+ this.lastVallue = initValue;
+ }
+
+ public void Stop()
+ {
+ isRunning = false;
+ }
+
+ public void Run()
+ {
+ isRunning = true;
+ Task.Run(() =>
+ {
+ while (isRunning)
+ {
+ var res = client.ReadInt16(Address);
+ if (res.IsSuccess)
+ {
+ int val = res.Content;
+ if (val != lastVallue)
+ {
+ DataChanged?.Invoke(this, new DataChangedArg<int>
+ {
+ oldValue = lastVallue,
+ newValue = val
+ });
+ lastVallue = val;
+ }
+ }
+ Thread.Sleep(Timeout);
+ }
+ });
+ }
+ }
+}
diff --git a/PLCHelper/PLCHelper.csproj b/PLCHelper/PLCHelper.csproj
index 691f096..a90a1dd 100644
--- a/PLCHelper/PLCHelper.csproj
+++ b/PLCHelper/PLCHelper.csproj
@@ -45,6 +45,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Entity\PLCTagData.cs" />
+ <Compile Include="BoolReader.cs" />
+ <Compile Include="IntReader.cs" />
<Compile Include="ModbusHelper.cs" />
<Compile Include="NXI_5010Helper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
diff --git a/Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs b/Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs
index 93c2289..b518c73 100644
--- a/Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs
+++ b/Plugin.BurnIn/ManualDebug/ManualDebug.xaml.cs
@@ -6,14 +6,18 @@
using Plugin.BurnIn.Model;
using Plugin.TsSupport;
using SuperSocket.SocketBase.Config;
+using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Configuration;
using System.Globalization;
+using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
+using System.Xml.Linq;
namespace Plugin.BurnIn
{
@@ -219,16 +223,39 @@
});
}
+ private int GetConfigPort()
+ {
+ try
+ {
+ var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig");
+ if (customConfig.ContainsKey("PortStart"))
+ {
+ int port = int.Parse(customConfig["PortStart"].ToString()) - 1;
+ return port;
+ }
+ else
+ {
+ return 6659;
+ }
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"读取起始端口错误: {ex.Message} {ex.StackTrace} 使用默认端口6659");
+ return 6659;
+ }
+ }
+
/// <summary>
/// 启动Tcp服务器
/// </summary>
[RelayCommand]
private void StartTcpServer()
{
+ int port = GetConfigPort();
var serverConfig = new ServerConfig()
{
MaxRequestLength = 1024 * 1024,
- Port = 7777
+ Port = port
};
try
{
@@ -247,6 +274,7 @@
return;
}
ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}");
}
else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running)
{
@@ -261,11 +289,13 @@
return;
}
ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}");
}
}
catch (Exception ex)
{
ChangeTcpServerStatus("ERROR", $"{ex.Message}");
+ AppendLog(LogLevel.Error, $"数据接收服务器启动错误: {ex.Message} {ex.StackTrace}");
tcpServer?.Stop();
tcpServer = null;
}
@@ -613,6 +643,7 @@
{
executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true);
executionWrapper?.WaitForExit();
+ executionWrapper?.Dispose();
tcpServer?.Stop();
}
catch (Exception ex)
diff --git a/Plugin.BurnIn/Plugin.BurnIn.csproj b/Plugin.BurnIn/Plugin.BurnIn.csproj
index 8a28ded..3765916 100644
--- a/Plugin.BurnIn/Plugin.BurnIn.csproj
+++ b/Plugin.BurnIn/Plugin.BurnIn.csproj
@@ -8,8 +8,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
- <AssemblyVersion>1.0.0.2</AssemblyVersion>
- <FileVersion>1.0.0.2</FileVersion>
+ <AssemblyVersion>1.0.0.3</AssemblyVersion>
+ <FileVersion>1.0.0.3</FileVersion>
<IncludeSymbols>False</IncludeSymbols>
</PropertyGroup>
diff --git a/Plugin.BurnIn/TestResultView.xaml.cs b/Plugin.BurnIn/TestResultView.xaml.cs
index 9d26b74..eb767aa 100644
--- a/Plugin.BurnIn/TestResultView.xaml.cs
+++ b/Plugin.BurnIn/TestResultView.xaml.cs
@@ -216,11 +216,11 @@
}
else
{
- executionWrapper = new TsExecutionWrapper();
+ executionWrapper = new TeststandWrapper();
}
executionWrapper.UIMessageEvent += ExecutionWrapper_UIMessageEvent;
- if (executionWrapper is TsExecutionWrapper tsWrapper)
+ if (executionWrapper is TeststandWrapper tsWrapper)
{
((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).BeginInit();
host.Child = tsWrapper.AxApplicationMgr;
@@ -631,7 +631,7 @@
public void ReleaseResource()
{
executionWrapper.UIMessageEvent -= ExecutionWrapper_UIMessageEvent;
-
+ executionWrapper.Dispose();
foreach (var table in tables)
{
table.RelaseResource();
diff --git a/Plugin.CurrentSensorControl/Entity/BaseConfig.cs b/Plugin.CurrentSensorControl/Entity/BaseConfig.cs
index 5d41679..80ebe5a 100644
--- a/Plugin.CurrentSensorControl/Entity/BaseConfig.cs
+++ b/Plugin.CurrentSensorControl/Entity/BaseConfig.cs
@@ -45,7 +45,7 @@
try
{
value = ParameterManager.GetCommonParameterByName("BurnInOvenPort");
- config.PortStart = int.Parse(value.Value);
+ config.PortStart = value == null ? config.PortStart : int.Parse(value.Value);
}
catch (Exception ex)
{
diff --git a/Plugin.DeviceConfig/BatchGenerateModal.xaml b/Plugin.DeviceConfig/BatchGenerateModal.xaml
new file mode 100644
index 0000000..dd7dd35
--- /dev/null
+++ b/Plugin.DeviceConfig/BatchGenerateModal.xaml
@@ -0,0 +1,175 @@
+<pu:WindowX x:Class="Plugin.DeviceConfig.BatchGenerateModal"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:local="clr-namespace:Plugin.DeviceConfig"
+ mc:Ignorable="d"
+ Title="鎵归噺鐢熸垚璁惧"
+ SizeToContent="Height"
+ Width="780"
+ WindowStartupLocation="CenterOwner"
+ Style="{StaticResource ModalWindow}">
+ <pu:WindowX.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </pu:WindowX.Resources>
+
+ <Grid Margin="16">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 鍙傛暟杈撳叆鍖� -->
+ <GroupBox Grid.Row="0" Header="鐢熸垚鍙傛暟" FontWeight="SemiBold" Margin="0,0,0,12">
+ <Grid Margin="8">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="120"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="120"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="璁惧绫诲瀷 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <ComboBox Grid.Row="0" Grid.Column="1" x:Name="cbDeviceType" VerticalAlignment="Center" Margin="4,0"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="椹卞姩绫诲瀷" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="0" Grid.Column="3" x:Name="tbDriverType" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="浼犺緭鏂瑰紡 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <ComboBox Grid.Row="1" Grid.Column="1" x:Name="cbTransport" VerticalAlignment="Center" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="2" Text="璁惧鏁伴噺 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <pu:NumberInput Grid.Row="1" Grid.Column="3" x:Name="niDeviceCount" VerticalAlignment="Center"
+ Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="1" Maximum="100" Value="1"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="姣忚澶囬�氶亾鏁�" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <pu:NumberInput Grid.Row="2" Grid.Column="1" x:Name="niChannelsPerDevice" VerticalAlignment="Center"
+ Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="0" Maximum="64" Value="1"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2" Text="璧峰绔彛鍙�" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <pu:NumberInput Grid.Row="2" Grid.Column="3" x:Name="niStartPort" VerticalAlignment="Center"
+ Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="1" Maximum="65535" Value="30000"/>
+
+ <TextBlock Grid.Row="3" Grid.Column="0" Text="鍦板潃鐢熸垚瑙勫垯" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" x:Name="tbAddressRule" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"
+ pu:TextBoxHelper.Watermark="渚�: 192.168.10.{0}锛寋0}涓哄簭鍙峰崰浣嶇"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="0" Text="Slot鏄犲皠璧风偣" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="4" Grid.Column="1" x:Name="tbSlotStart" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"
+ pu:TextBoxHelper.Watermark="渚�: Slot001"/>
+
+ <TextBlock Grid.Row="4" Grid.Column="2" Text="璁惧ID鍓嶇紑" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="4" Grid.Column="3" x:Name="tbDeviceIdPrefix" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"
+ pu:TextBoxHelper.Watermark="渚�: PSU"/>
+ </Grid>
+ </GroupBox>
+
+ <!-- 棰勮鍖� -->
+ <GroupBox Grid.Row="1" Header="鐢熸垚棰勮" FontWeight="SemiBold" Margin="0,0,0,12">
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="200"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" x:Name="tbConflictMsg" Foreground="Red" Margin="0,0,0,4"
+ TextWrapping="Wrap" Visibility="Collapsed"/>
+
+ <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="previewListView"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="50" Header="#">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Converter={StaticResource lvRowNumber},
+ RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="140" Header="DeviceId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="Kind">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Kind}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="180" Header="Endpoint">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Endpoint}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="閫氶亾鏁�">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelCount}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="Slot璧风偣">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding SlotStart}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </GroupBox>
+
+ <!-- 搴曢儴鎸夐挳 -->
+ <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10" Margin="0,12,0,0">
+ <Button x:Name="previewBtn" Click="PreviewBtn_Click" Content="鐢熸垚棰勮"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ <Button x:Name="saveBtn" Click="SaveBtn_Click" Content="纭淇濆瓨"
+ Style="{StaticResource AddBtn}" Width="90"/>
+ <Button x:Name="cancelBtn" Click="CancelBtn_Click" Content="鍙栨秷"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ </StackPanel>
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.DeviceConfig/BatchGenerateModal.xaml.cs b/Plugin.DeviceConfig/BatchGenerateModal.xaml.cs
new file mode 100644
index 0000000..b5eb211
--- /dev/null
+++ b/Plugin.DeviceConfig/BatchGenerateModal.xaml.cs
@@ -0,0 +1,252 @@
+using HMI.UI.Util;
+using Panuon.WPF.UI;
+using Plugin.DeviceConfig.Entity;
+using Plugin.DeviceConfig.Model;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+
+namespace Plugin.DeviceConfig
+{
+ /// <summary>
+ /// BatchGenerateModal.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class BatchGenerateModal : WindowX
+ {
+ private DeviceConfigModel _model;
+ private ObservableCollection<BatchPreviewItem> _previewItems = new();
+
+ public BatchGenerateModal()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 鎵撳紑寮圭獥
+ /// </summary>
+ public bool? Open(DeviceConfigModel model)
+ {
+ _model = model;
+
+ // 鍒濆鍖栨灇涓句笅鎷�
+ cbDeviceType.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast<DeviceKind>().ToList();
+ cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast<TransportType>().ToList();
+
+ // 榛樿鍊�
+ cbDeviceType.SelectedIndex = 0;
+ cbTransport.SelectedIndex = 0;
+ tbAddressRule.Text = "192.168.10.{0}";
+ tbSlotStart.Text = "Slot001";
+ tbDeviceIdPrefix.Text = "DEV";
+
+ previewListView.ItemsSource = _previewItems;
+
+ return ShowDialog();
+ }
+
+ #region 棰勮
+
+ private void PreviewBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ GeneratePreview();
+ }
+ catch (Exception ex)
+ {
+ tbConflictMsg.Text = $"鐢熸垚棰勮澶辫触: {ex.Message}";
+ tbConflictMsg.Visibility = Visibility.Visible;
+ }
+ }
+
+ private void GeneratePreview()
+ {
+ _previewItems.Clear();
+ tbConflictMsg.Visibility = Visibility.Collapsed;
+
+ if (cbDeviceType.SelectedItem is not DeviceKind kind)
+ {
+ ShowConflict("璇烽�夋嫨璁惧绫诲瀷");
+ return;
+ }
+ if (cbTransport.SelectedItem is not TransportType transport)
+ {
+ ShowConflict("璇烽�夋嫨浼犺緭鏂瑰紡");
+ return;
+ }
+
+ int count = (int)(niDeviceCount.Value ?? 1);
+ int channelsPerDevice = (int)(niChannelsPerDevice.Value ?? 1);
+ int startPort = (int)(niStartPort.Value ?? 30000);
+ string addressRule = tbAddressRule.Text?.Trim() ?? string.Empty;
+ string slotStart = tbSlotStart.Text?.Trim() ?? string.Empty;
+ string prefix = tbDeviceIdPrefix.Text?.Trim() ?? "DEV";
+ string driverType = tbDriverType.Text?.Trim() ?? string.Empty;
+
+ // 妫�娴嬪凡鏈夎澶嘔D
+ var existingDevices = _model.GetAllDevices();
+ var existingIds = new HashSet<string>(existingDevices.Select(d => d.DeviceId));
+ var existingEndpoints = new HashSet<string>(existingDevices.Select(d => d.Endpoint));
+
+ var conflicts = new List<string>();
+ var generatedIds = new HashSet<string>();
+ var generatedEndpoints = new HashSet<string>();
+
+ for (int i = 1; i <= count; i++)
+ {
+ var deviceId = $"{prefix}{i:D3}";
+ var endpoint = string.Format(addressRule, i);
+ if (transport == TransportType.Tcp && !string.IsNullOrEmpty(addressRule))
+ {
+ endpoint = $"{endpoint}:{startPort + i - 1}";
+ }
+ else if (transport == TransportType.Serial)
+ {
+ endpoint = $"COM{i}";
+ }
+
+ // 瑙f瀽 SlotStart 鐨勫簭鍙烽儴鍒嗗苟閫掑
+ var slotPrefix = new string(slotStart.TakeWhile(c => !char.IsDigit(c)).ToArray());
+ var slotNumStr = new string(slotStart.SkipWhile(c => !char.IsDigit(c)).ToArray());
+ int.TryParse(slotNumStr, out int startNum);
+ var slotId = $"{slotPrefix}{(startNum + i - 1):D3}";
+
+ // 妫�娴嬪啿绐�
+ if (existingIds.Contains(deviceId) || generatedIds.Contains(deviceId))
+ conflicts.Add($"璁惧ID鍐茬獊: {deviceId}");
+ if (existingEndpoints.Contains(endpoint) || generatedEndpoints.Contains(endpoint))
+ conflicts.Add($"Endpoint鍐茬獊: {endpoint}");
+
+ generatedIds.Add(deviceId);
+ generatedEndpoints.Add(endpoint);
+
+ _previewItems.Add(new BatchPreviewItem
+ {
+ DeviceId = deviceId,
+ Kind = kind,
+ DriverType = driverType,
+ Transport = transport,
+ Endpoint = endpoint,
+ ChannelCount = channelsPerDevice,
+ SlotStart = slotId,
+ });
+ }
+
+ if (conflicts.Count > 0)
+ {
+ ShowConflict(string.Join("\n", conflicts));
+ }
+ }
+
+ private void ShowConflict(string msg)
+ {
+ tbConflictMsg.Text = msg;
+ tbConflictMsg.Visibility = Visibility.Visible;
+ }
+
+ #endregion
+
+ #region 淇濆瓨
+
+ private void SaveBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (_previewItems.Count == 0)
+ {
+ ToastHelper.Warning("璇峰厛鐢熸垚棰勮");
+ return;
+ }
+
+ if (tbConflictMsg.Visibility == Visibility.Visible)
+ {
+ if (MsgBoxHelper.WarnMessage(this, "瀛樺湪鍐茬獊椤癸紝鏄惁浠嶈缁х画淇濆瓨锛�") != MessageBoxResult.OK)
+ {
+ return;
+ }
+ }
+
+ int channelsPerDevice = (int)(niChannelsPerDevice.Value ?? 1);
+ string slotStart = tbSlotStart.Text?.Trim() ?? string.Empty;
+
+ foreach (var item in _previewItems)
+ {
+ // 淇濆瓨璁惧
+ var device = new DeviceDefinition
+ {
+ DeviceId = item.DeviceId,
+ Kind = item.Kind,
+ DriverType = item.DriverType,
+ Transport = item.Transport,
+ Endpoint = item.Endpoint,
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now,
+ };
+ _model.SaveDevice(device);
+
+ // 淇濆瓨閫氶亾
+ for (int ch = 0; ch < channelsPerDevice; ch++)
+ {
+ var channel = new DeviceChannel
+ {
+ DeviceId = item.DeviceId,
+ ChannelId = ch,
+ Capability = $"CH{ch}",
+ MaxVoltage = 0,
+ MaxCurrent = 0,
+ };
+ _model.SaveChannel(channel);
+ }
+
+ // 淇濆瓨 Slot 鏄犲皠
+ if (!string.IsNullOrEmpty(item.SlotStart))
+ {
+ var mapping = new SlotDeviceMapping
+ {
+ SlotId = item.SlotStart,
+ Capability = "Default",
+ Version = "1.0",
+ DeviceId = item.DeviceId,
+ ChannelId = 0,
+ MappingVersion = "1.0",
+ };
+ _model.SaveSlotMapping(mapping);
+ }
+ }
+
+ ToastHelper.Success($"鎴愬姛鐢熸垚 {_previewItems.Count} 涓澶�");
+ DialogResult = true;
+ Close();
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"淇濆瓨澶辫触: {ex.Message}");
+ }
+ }
+
+ private void CancelBtn_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// 鎵归噺鐢熸垚棰勮椤�
+ /// </summary>
+ public class BatchPreviewItem
+ {
+ public string DeviceId { get; set; } = string.Empty;
+ public DeviceKind Kind { get; set; }
+ public string DriverType { get; set; } = string.Empty;
+ public TransportType Transport { get; set; }
+ public string Endpoint { get; set; } = string.Empty;
+ public int ChannelCount { get; set; }
+ public string SlotStart { get; set; } = string.Empty;
+ }
+}
diff --git a/Plugin.DeviceConfig/DeviceConfig.xaml b/Plugin.DeviceConfig/DeviceConfig.xaml
new file mode 100644
index 0000000..4dab71d
--- /dev/null
+++ b/Plugin.DeviceConfig/DeviceConfig.xaml
@@ -0,0 +1,293 @@
+<hb:SimpleMaskControl x:Class="Plugin.DeviceConfig.DeviceConfig"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:local="clr-namespace:Plugin.DeviceConfig"
+ mc:Ignorable="d"
+ Loaded="SimpleMaskControl_Loaded"
+ d:DesignHeight="700" d:DesignWidth="1200">
+ <hb:SimpleMaskControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ <Style x:Key="TitleBox" TargetType="TextBlock">
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="SemiBold"/>
+ <Setter Property="Padding" Value="8,4"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+ </ResourceDictionary>
+ </hb:SimpleMaskControl.Resources>
+
+ <Grid dp:GridHelper.Space="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 椤堕儴宸ュ叿鏍� -->
+ <DockPanel Grid.Row="0">
+ <StackPanel dp:StackPanelHelper.ChildSpace="10" DockPanel.Dock="Right" Orientation="Horizontal">
+ <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/>
+ <Button Style="{StaticResource EditBtn}" x:Name="editBtn" Content="缂栬緫" Click="Button_Click"/>
+ <Button Style="{StaticResource DelBtn}" x:Name="delBtn" Content="鍒犻櫎" Click="Button_Click"/>
+ <Button x:Name="batchBtn" Click="Button_Click" Content="鎵归噺鐢熸垚"
+ Style="{StaticResource AddBtn}"/>
+ <Button Style="{StaticResource ImportBtn}" x:Name="importBtn" Click="Button_Click"/>
+ <Button Style="{StaticResource ExportBtn}" x:Name="exportBtn" Click="Button_Click"/>
+ <Button x:Name="versionBtn" Click="Button_Click" Content="鐗堟湰绠$悊"
+ Style="{StaticResource RfhBtn}"/>
+ <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="Button_Click"/>
+ </StackPanel>
+ <TextBlock Style="{StaticResource TitleBox}" Text="浠櫒浠〃娉ㄥ唽"/>
+ </DockPanel>
+
+ <!-- 涓讳綋涓夋爮鍖哄煙 -->
+ <Grid Grid.Row="1">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="250"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="300"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 宸︽爮锛氳澶囧垎绫绘爲 -->
+ <Border Grid.Column="0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="0,0,4,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBox x:Name="searchBox" Grid.Row="0" Margin="6"
+ pu:TextBoxHelper.Watermark="鎼滅储璁惧..."
+ Style="{StaticResource EditorTextBoxStyle}"
+ TextChanged="SearchBox_TextChanged"/>
+ <TreeView x:Name="deviceTree" Grid.Row="1" Margin="4"
+ SelectedItemChanged="DeviceTree_SelectedItemChanged">
+ <TreeView.ItemTemplate>
+ <HierarchicalDataTemplate ItemsSource="{Binding Children}">
+ <TextBlock Text="{Binding Name}" FontWeight="SemiBold"/>
+ </HierarchicalDataTemplate>
+ </TreeView.ItemTemplate>
+ </TreeView>
+ </Grid>
+ </Border>
+
+ <!-- 涓爮锛氳澶囪鎯� -->
+ <Border Grid.Column="1" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="2,0,2,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 璁惧鍩烘湰淇℃伅琛ㄥ崟 -->
+ <GroupBox Grid.Row="0" Header="璁惧淇℃伅" Margin="6" FontWeight="SemiBold">
+ <Grid Margin="8">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="120"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="120"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="璁惧ID" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbDeviceId" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="璁惧绫诲瀷" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="0" Grid.Column="3" x:Name="tbKind" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="椹卞姩绫诲瀷" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbDriverType" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="2" Text="浼犺緭鏂瑰紡" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="1" Grid.Column="3" x:Name="tbTransport" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="杩炴帴绔偣" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" x:Name="tbEndpoint" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="2" Text="鐢熸晥鐗堟湰" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="2" Grid.Column="3" x:Name="tbEffectiveVersion" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/>
+ </Grid>
+ </GroupBox>
+
+ <!-- 閫氶亾鏄犲皠琛� -->
+ <Grid Grid.Row="1" Margin="6">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Grid.Row="0" Text="閫氶亾鍒楄〃" FontWeight="SemiBold" Margin="0,0,0,4"/>
+ <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="channelListView"
+ ItemsSource="{Binding Channels}"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="50" Header="#">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Converter={StaticResource lvRowNumber},
+ RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="150" Header="DeviceId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="ChannelId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="200" Header="Capability">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="MaxVoltage">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxVoltage, StringFormat=F2}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="MaxCurrent">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxCurrent, StringFormat=F2}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Grid>
+ </Border>
+
+ <!-- 鍙虫爮锛歋lot鏄犲皠琛� -->
+ <Border Grid.Column="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="4,0,0,0">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <TextBlock Grid.Row="0" Text="Slot 鏄犲皠" FontWeight="SemiBold" Margin="6"/>
+ <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="slotListView"
+ ItemsSource="{Binding SlotMappings}"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard"
+ Margin="4">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="80" Header="SlotId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding SlotId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="Capability">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="DeviceId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="60" Header="ChId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="MapVer">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MappingVersion}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+ </Grid>
+
+ <!-- 搴曢儴鐘舵�佹爮 -->
+ <DockPanel Grid.Row="2" Margin="0,4,0,0">
+ <StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="6">
+ <TextBlock Text="褰撳墠鐗堟湰鐘舵�侊細" FontWeight="SemiBold" VerticalAlignment="Center"/>
+ <Border Background="{DynamicResource SecondPrimaryColor}" CornerRadius="3" Padding="8,2">
+ <TextBlock x:Name="tbVersionStatus" Text="{Binding CurrentVersionStatus}" Foreground="White" FontWeight="Bold"/>
+ </Border>
+ </StackPanel>
+ <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10">
+ <Button x:Name="saveDraftBtn" Click="Button_Click" Content="淇濆瓨鑽夌"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ <Button x:Name="publishBtn" Click="Button_Click" Content="鍙戝竷"
+ Style="{StaticResource AddBtn}" Width="70"/>
+ <Button x:Name="applyBtn" Click="Button_Click" Content="搴旂敤"
+ Style="{StaticResource AddBtn}" Width="70"/>
+ </StackPanel>
+ </DockPanel>
+ </Grid>
+</hb:SimpleMaskControl>
diff --git a/Plugin.DeviceConfig/DeviceConfig.xaml.cs b/Plugin.DeviceConfig/DeviceConfig.xaml.cs
new file mode 100644
index 0000000..e4d3357
--- /dev/null
+++ b/Plugin.DeviceConfig/DeviceConfig.xaml.cs
@@ -0,0 +1,611 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using HMI.Base;
+using HMI.Core.Util;
+using HMI.UI.Util;
+using Plugin.DeviceConfig.Entity;
+using Plugin.DeviceConfig.Model;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+
+namespace Plugin.DeviceConfig
+{
+ /// <summary>
+ /// DeviceConfig.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ [ObservableObject]
+ public partial class DeviceConfig : SimpleMaskControl
+ {
+ private DeviceConfigModel _model;
+
+ [ObservableProperty]
+ private ObservableCollection<DeviceDefinition> devices = new();
+
+ [ObservableProperty]
+ private ObservableCollection<DeviceChannel> channels = new();
+
+ [ObservableProperty]
+ private ObservableCollection<SlotDeviceMapping> slotMappings = new();
+
+ [ObservableProperty]
+ private DeviceDefinition selectedDevice;
+
+ [ObservableProperty]
+ private string currentVersionStatus = "Draft";
+
+ [ObservableProperty]
+ private ObservableCollection<TreeNode> treeCategories = new();
+
+ public DeviceConfig()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 寮哄埗鍒锋柊鏁版嵁锛堜緵闆嗘垚娴嬭瘯棰勭疆璁惧鍚庤皟鐢級
+ /// </summary>
+ public void ForceRefreshForTest()
+ {
+ try { RefreshAllData(); }
+ catch (Exception ex) { log.Error("ForceRefreshForTest Error.", ex); }
+ }
+
+ public override void InitFinish()
+ {
+ base.InitFinish();
+ try
+ {
+ _model = new DeviceConfigModel();
+ var dbPath = Path.Combine(BaseFolder, "data.db");
+ _model.InitDatabase(dbPath);
+ LoadTreeCategories();
+ RefreshAllData();
+ }
+ catch (Exception ex)
+ {
+ log.Error("InitFinish Error.", ex);
+ ToastHelper.Error($"鍒濆鍖栧け璐�: {ex.Message}");
+ }
+ }
+
+ #region 鏍戝垎绫诲姞杞�
+
+ private void LoadTreeCategories()
+ {
+ TreeCategories = new ObservableCollection<TreeNode>
+ {
+ new TreeNode { Name = "鍏ㄩ儴璁惧", Kind = null, Children = new ObservableCollection<TreeNode>() },
+ new TreeNode { Name = "鐢垫簮绯荤粺", Kind = DeviceKind.PowerSupply, Children = new ObservableCollection<TreeNode>() },
+ new TreeNode { Name = "CAN/LIN", Kind = DeviceKind.CanDevice, Children = new ObservableCollection<TreeNode>() },
+ new TreeNode { Name = "PLC/IO", Kind = DeviceKind.PlcDevice, Children = new ObservableCollection<TreeNode>() },
+ new TreeNode { Name = "娓╃/DAQ", Kind = null, Children = new ObservableCollection<TreeNode>(),
+ FilterKinds = new[] { DeviceKind.Chamber, DeviceKind.DAQ } },
+ };
+ deviceTree.ItemsSource = TreeCategories;
+ }
+
+ #endregion
+
+ #region 鏁版嵁鍔犺浇
+
+ private void RefreshAllData()
+ {
+ try
+ {
+ var deviceList = _model.GetAllDevices();
+ Devices = new ObservableCollection<DeviceDefinition>(deviceList);
+ SlotMappings = new ObservableCollection<SlotDeviceMapping>(_model.GetSlotMappings());
+ RefreshTreeDevices();
+ LoadLatestVersionStatus();
+ }
+ catch (Exception ex)
+ {
+ log.Error("RefreshAllData Error.", ex);
+ ToastHelper.Error($"鍔犺浇鏁版嵁澶辫触: {ex.Message}");
+ }
+ }
+
+ private void RefreshTreeDevices()
+ {
+ foreach (var cat in TreeCategories)
+ {
+ cat.Children.Clear();
+ IEnumerable<DeviceDefinition> filtered = Devices;
+ if (cat.Kind.HasValue)
+ {
+ filtered = Devices.Where(d => d.Kind == cat.Kind.Value);
+ }
+ else if (cat.FilterKinds != null)
+ {
+ filtered = Devices.Where(d => cat.FilterKinds.Contains(d.Kind));
+ }
+ foreach (var dev in filtered)
+ {
+ cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev });
+ }
+ }
+ }
+
+ private void LoadLatestVersionStatus()
+ {
+ try
+ {
+ var versions = _model.GetConfigVersions();
+ var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective);
+ if (effective != null)
+ {
+ CurrentVersionStatus = "Effective";
+ }
+ else if (versions.Any(v => v.Status == VersionStatus.Published))
+ {
+ CurrentVersionStatus = "Published";
+ }
+ else
+ {
+ CurrentVersionStatus = "Draft";
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("LoadLatestVersionStatus Error.", ex);
+ }
+ }
+
+ #endregion
+
+ #region 浜嬩欢澶勭悊
+
+ private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ // InitFinish 宸插姞杞芥暟鎹紝姝ゅ浠呭仛涓�娆℃�� UI 鍒濆鍖�
+ Loaded -= SimpleMaskControl_Loaded;
+ }
+
+ private void DeviceTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
+ {
+ if (e.NewValue is TreeNode node && node.Tag is DeviceDefinition dev)
+ {
+ SelectDevice(dev);
+ }
+ }
+
+ private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ try
+ {
+ var keyword = searchBox.Text?.Trim() ?? string.Empty;
+ if (string.IsNullOrEmpty(keyword))
+ {
+ RefreshTreeDevices();
+ return;
+ }
+ foreach (var cat in TreeCategories)
+ {
+ cat.Children.Clear();
+ var matched = Devices.Where(d =>
+ d.DeviceId.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 ||
+ d.Endpoint.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 ||
+ d.DriverType.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0);
+ if (cat.Kind.HasValue)
+ matched = matched.Where(d => d.Kind == cat.Kind.Value);
+ else if (cat.FilterKinds != null)
+ matched = matched.Where(d => cat.FilterKinds.Contains(d.Kind));
+
+ foreach (var dev in matched)
+ cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev });
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("SearchBox_TextChanged Error.", ex);
+ }
+ }
+
+ private void SelectDevice(DeviceDefinition dev)
+ {
+ try
+ {
+ SelectedDevice = dev;
+ tbDeviceId.Text = dev.DeviceId;
+ tbKind.Text = dev.Kind.ToString();
+ tbDriverType.Text = dev.DriverType;
+ tbTransport.Text = dev.Transport.ToString();
+ tbEndpoint.Text = dev.Endpoint;
+ tbEffectiveVersion.Text = dev.EffectiveVersion;
+
+ var chList = _model.GetChannelsByDevice(dev.DeviceId);
+ Channels = new ObservableCollection<DeviceChannel>(chList);
+ }
+ catch (Exception ex)
+ {
+ log.Error("SelectDevice Error.", ex);
+ ToastHelper.Error($"鍔犺浇璁惧璇︽儏澶辫触: {ex.Message}");
+ }
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Button btn = sender as Button;
+ try
+ {
+ switch (btn?.Name)
+ {
+ case "addBtn":
+ OpenDeviceEditModal(null);
+ break;
+ case "editBtn":
+ EditSelectedDevice();
+ break;
+ case "delBtn":
+ DeleteSelectedDevice();
+ break;
+ case "batchBtn":
+ OpenBatchGenerateModal();
+ break;
+ case "importBtn":
+ ImportDevices();
+ break;
+ case "exportBtn":
+ ExportDevices();
+ break;
+ case "versionBtn":
+ OpenVersionManageModal();
+ break;
+ case "rfhBtn":
+ RefreshAllData();
+ ToastHelper.Success("鍒锋柊鎴愬姛");
+ break;
+ case "saveDraftBtn":
+ SaveDraft();
+ break;
+ case "publishBtn":
+ PublishVersion();
+ break;
+ case "applyBtn":
+ ApplyVersion();
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("Button_Click Error.", ex);
+ ToastHelper.Error($"鎿嶄綔澶辫触: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 寮圭獥
+
+ private void OpenDeviceEditModal(DeviceDefinition device)
+ {
+ try
+ {
+ device ??= new DeviceDefinition();
+ var win = new DeviceEditModal
+ {
+ Title = string.IsNullOrEmpty(device.DeviceId) ? "鏂板璁惧" : "缂栬緫璁惧",
+ Owner = Window.GetWindow(this)
+ };
+ if (win.Open(device, _model) == true)
+ {
+ RefreshAllData();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("OpenDeviceEditModal Error.", ex);
+ ToastHelper.Error($"鎵撳紑缂栬緫寮圭獥澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 缂栬緫褰撳墠閫変腑鐨勮澶�
+ /// </summary>
+ private void EditSelectedDevice()
+ {
+ try
+ {
+ if (SelectedDevice == null)
+ {
+ ToastHelper.Warning("璇峰厛鍦ㄥ乏渚ц澶囨爲涓�夋嫨涓�涓澶�");
+ return;
+ }
+
+ // 浣跨敤褰撳墠璁惧淇℃伅鐨勫壇鏈伩鍏嶇洿鎺ヤ慨鏀瑰唴閮ㄦ暟鎹�
+ var editingDevice = new DeviceDefinition
+ {
+ DeviceId = SelectedDevice.DeviceId,
+ Kind = SelectedDevice.Kind,
+ DriverType = SelectedDevice.DriverType,
+ Transport = SelectedDevice.Transport,
+ Endpoint = SelectedDevice.Endpoint,
+ EffectiveVersion = SelectedDevice.EffectiveVersion,
+ CreatedAt = SelectedDevice.CreatedAt,
+ UpdatedAt = SelectedDevice.UpdatedAt
+ };
+
+ var win = new DeviceEditModal
+ {
+ Title = $"缂栬緫璁惧 - {SelectedDevice.DeviceId}",
+ Owner = Window.GetWindow(this)
+ };
+ if (win.Open(editingDevice, _model) == true)
+ {
+ RefreshAllData();
+ ToastHelper.Success($"璁惧 {editingDevice.DeviceId} 宸叉洿鏂�");
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("EditSelectedDevice Error.", ex);
+ ToastHelper.Error($"缂栬緫璁惧澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 鍒犻櫎褰撳墠閫変腑鐨勮澶囷紙鍚屾椂娓呯悊鐩稿叧閫氶亾涓� Slot 鏄犲皠锛�
+ /// </summary>
+ private void DeleteSelectedDevice()
+ {
+ try
+ {
+ if (SelectedDevice == null)
+ {
+ ToastHelper.Warning("璇峰厛鍦ㄥ乏渚ц澶囨爲涓�夋嫨涓�涓澶�");
+ return;
+ }
+
+ var deviceId = SelectedDevice.DeviceId;
+ if (MsgBoxHelper.WarnMessage(Window.GetWindow(this),
+ $"纭畾瑕佸垹闄よ澶� [{deviceId}] 鍚楋紵\n璇ユ搷浣滃皢鍚屾椂鍒犻櫎鐩稿叧鐨勯�氶亾涓� Slot 鏄犲皠锛屼笖涓嶅彲鎭㈠銆�") != MessageBoxResult.OK)
+ {
+ return;
+ }
+
+ // 鍏堝垹闄ゅ叧鑱旂殑閫氶亾涓� Slot 鏄犲皠
+ var channels = _model.GetChannelsByDevice(deviceId);
+ foreach (var ch in channels)
+ {
+ _model.DeleteChannel(deviceId, ch.ChannelId);
+ }
+
+ var mappings = _model.GetSlotMappings();
+ foreach (var m in mappings.Where(x => x.DeviceId == deviceId))
+ {
+ _model.DeleteSlotMapping(m.SlotId, m.Capability, m.Version);
+ }
+
+ // 鍒犻櫎璁惧鏈韩
+ _model.DeleteDevice(deviceId);
+
+ SelectedDevice = null;
+ ClearDeviceDetails();
+ RefreshAllData();
+ ToastHelper.Success($"璁惧 {deviceId} 宸插垹闄�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("DeleteSelectedDevice Error.", ex);
+ ToastHelper.Error($"鍒犻櫎璁惧澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 娓呯┖涓爮璁惧璇︽儏灞曠ず
+ /// </summary>
+ private void ClearDeviceDetails()
+ {
+ tbDeviceId.Text = string.Empty;
+ tbKind.Text = string.Empty;
+ tbDriverType.Text = string.Empty;
+ tbTransport.Text = string.Empty;
+ tbEndpoint.Text = string.Empty;
+ tbEffectiveVersion.Text = string.Empty;
+ Channels = new ObservableCollection<DeviceChannel>();
+ }
+
+ private void OpenBatchGenerateModal()
+ {
+ try
+ {
+ var win = new BatchGenerateModal
+ {
+ Title = "鎵归噺鐢熸垚璁惧",
+ Owner = Window.GetWindow(this)
+ };
+ if (win.Open(_model) == true)
+ {
+ RefreshAllData();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("OpenBatchGenerateModal Error.", ex);
+ ToastHelper.Error($"鎵撳紑鎵归噺鐢熸垚寮圭獥澶辫触: {ex.Message}");
+ }
+ }
+
+ private void OpenVersionManageModal()
+ {
+ try
+ {
+ var win = new VersionManageModal
+ {
+ Title = "鐗堟湰绠$悊",
+ Owner = Window.GetWindow(this)
+ };
+ if (win.Open(_model) == true)
+ {
+ RefreshAllData();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("OpenVersionManageModal Error.", ex);
+ ToastHelper.Error($"鎵撳紑鐗堟湰绠$悊寮圭獥澶辫触: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 鐗堟湰鎿嶄綔
+
+ private void SaveDraft()
+ {
+ try
+ {
+ var versionId = $"V{DateTime.Now:yyyyMMddHHmmss}";
+ var hash = _model.ComputeConfigHash();
+ var ver = new DeviceConfigVersion
+ {
+ VersionId = versionId,
+ Status = VersionStatus.Draft,
+ Hash = hash,
+ Author = Environment.UserName,
+ Timestamp = DateTime.Now,
+ DiffDescription = "淇濆瓨鑽夌"
+ };
+ _model.SaveConfigVersion(ver);
+ CurrentVersionStatus = "Draft";
+ ToastHelper.Success($"鑽夌宸蹭繚瀛�: {versionId}");
+ }
+ catch (Exception ex)
+ {
+ log.Error("SaveDraft Error.", ex);
+ ToastHelper.Error($"淇濆瓨鑽夌澶辫触: {ex.Message}");
+ }
+ }
+
+ private void PublishVersion()
+ {
+ try
+ {
+ var versions = _model.GetConfigVersions();
+ var draft = versions.FirstOrDefault(v => v.Status == VersionStatus.Draft);
+ if (draft == null)
+ {
+ ToastHelper.Warning("娌℃湁鍙彂甯冪殑鑽夌鐗堟湰");
+ return;
+ }
+ var hash = _model.ComputeConfigHash();
+ draft.Hash = hash;
+ draft.Status = VersionStatus.Published;
+ draft.Timestamp = DateTime.Now;
+ _model.SaveConfigVersion(draft);
+ CurrentVersionStatus = "Published";
+ ToastHelper.Success($"鐗堟湰 {draft.VersionId} 宸插彂甯�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("PublishVersion Error.", ex);
+ ToastHelper.Error($"鍙戝竷澶辫触: {ex.Message}");
+ }
+ }
+
+ private void ApplyVersion()
+ {
+ try
+ {
+ var versions = _model.GetConfigVersions();
+ var published = versions.FirstOrDefault(v => v.Status == VersionStatus.Published);
+ if (published == null)
+ {
+ ToastHelper.Warning("娌℃湁鍙簲鐢ㄧ殑宸插彂甯冪増鏈�");
+ return;
+ }
+
+ var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective);
+ if (effective != null)
+ {
+ if (MsgBoxHelper.WarnMessage(Window.GetWindow(this),
+ $"褰撳墠宸叉湁鐢熸晥鐗堟湰 [{effective.VersionId}]锛屽簲鐢ㄦ柊鐗堟湰灏嗘浛鎹㈠畠锛屾槸鍚︾户缁紵") != MessageBoxResult.OK)
+ {
+ return;
+ }
+ effective.Status = VersionStatus.Published;
+ _model.SaveConfigVersion(effective);
+ }
+
+ published.Status = VersionStatus.Effective;
+ published.Timestamp = DateTime.Now;
+ _model.SaveConfigVersion(published);
+ CurrentVersionStatus = "Effective";
+ ToastHelper.Success($"鐗堟湰 {published.VersionId} 宸插簲鐢ㄤ负鐢熸晥鐗堟湰");
+ }
+ catch (Exception ex)
+ {
+ log.Error("ApplyVersion Error.", ex);
+ ToastHelper.Error($"搴旂敤鐗堟湰澶辫触: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 瀵煎叆瀵煎嚭
+
+ private void ImportDevices()
+ {
+ try
+ {
+ var ofd = new Microsoft.Win32.OpenFileDialog
+ {
+ Filter = "XML鏂囦欢|*.xml|JSON鏂囦欢|*.json"
+ };
+ if (ofd.ShowDialog() == true)
+ {
+ // TODO: 瀹炵幇瀵煎叆閫昏緫
+ ToastHelper.Success($"宸蹭粠 {Path.GetFileName(ofd.FileName)} 瀵煎叆");
+ RefreshAllData();
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("ImportDevices Error.", ex);
+ ToastHelper.Error($"瀵煎叆澶辫触: {ex.Message}");
+ }
+ }
+
+ private void ExportDevices()
+ {
+ try
+ {
+ var sfd = new Microsoft.Win32.SaveFileDialog
+ {
+ FileName = "DeviceConfig",
+ Filter = "XML鏂囦欢|*.xml"
+ };
+ if (sfd.ShowDialog() == true)
+ {
+ // TODO: 瀹炵幇瀵煎嚭閫昏緫
+ ToastHelper.Success("瀵煎嚭鎴愬姛");
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("ExportDevices Error.", ex);
+ ToastHelper.Error($"瀵煎嚭澶辫触: {ex.Message}");
+ }
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// 鏍戣妭鐐规ā鍨�
+ /// </summary>
+ public class TreeNode
+ {
+ public string Name { get; set; } = string.Empty;
+ public DeviceKind? Kind { get; set; }
+ public DeviceKind[] FilterKinds { get; set; }
+ public object Tag { get; set; }
+ public ObservableCollection<TreeNode> Children { get; set; } = new();
+ }
+}
diff --git a/Plugin.DeviceConfig/DeviceEditModal.xaml b/Plugin.DeviceConfig/DeviceEditModal.xaml
new file mode 100644
index 0000000..cb7c97d
--- /dev/null
+++ b/Plugin.DeviceConfig/DeviceEditModal.xaml
@@ -0,0 +1,182 @@
+<pu:WindowX x:Class="Plugin.DeviceConfig.DeviceEditModal"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:local="clr-namespace:Plugin.DeviceConfig"
+ mc:Ignorable="d"
+ Title="璁惧缂栬緫"
+ SizeToContent="Height"
+ Width="720"
+ WindowStartupLocation="CenterOwner"
+ Style="{StaticResource ModalWindow}">
+ <pu:WindowX.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </pu:WindowX.Resources>
+
+ <Grid Margin="16">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 璁惧鍩烘湰淇℃伅 -->
+ <GroupBox Grid.Row="0" Header="璁惧淇℃伅" FontWeight="SemiBold" Margin="0,0,0,12">
+ <Grid Margin="8">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ <RowDefinition Height="36"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="璁惧ID *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbDeviceId" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="璁惧绫诲瀷 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <ComboBox Grid.Row="0" Grid.Column="3" x:Name="cbKind" VerticalAlignment="Center" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="椹卞姩绫诲瀷" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbDriverType" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="2" Text="浼犺緭鏂瑰紡 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <ComboBox Grid.Row="1" Grid.Column="3" x:Name="cbTransport" VerticalAlignment="Center" Margin="4,0"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="杩炴帴绔偣 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/>
+ <TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" x:Name="tbEndpoint" VerticalAlignment="Center"
+ Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"
+ pu:TextBoxHelper.Watermark="渚�: 192.168.10.124:30000 鎴� COM3"/>
+ </Grid>
+ </GroupBox>
+
+ <!-- 閫氶亾鍒楄〃 -->
+ <GroupBox Grid.Row="1" Header="閫氶亾鍒楄〃" FontWeight="SemiBold" Margin="0,0,0,12">
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="180"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,4"
+ dp:StackPanelHelper.ChildSpace="8">
+ <Button x:Name="addChannelBtn" Click="AddChannel_Click" Content="+ 鏂板閫氶亾"
+ Style="{StaticResource AddBtn}" Width="100"/>
+ <Button x:Name="delChannelBtn" Click="DelChannel_Click" Content="鍒犻櫎閫氶亾"
+ Style="{StaticResource DelBtn}" Width="100"/>
+ </StackPanel>
+
+ <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="channelListView"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="50" Header="#">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Converter={StaticResource lvRowNumber},
+ RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="ChannelId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="200" Header="Capability">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="MaxVoltage">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxVoltage, StringFormat=F2}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="MaxCurrent">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxCurrent, StringFormat=F2}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="鎿嶄綔">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="6">
+ <Button x:Name="editChBtn" Style="{StaticResource EditLink}" Click="EditChannel_Click" Tag="{Binding}"/>
+ <Button x:Name="delChBtn" Style="{StaticResource DeleteLink}" Click="DeleteChannel_Click" Tag="{Binding}"/>
+ </StackPanel>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+
+ <!-- 閫氶亾缂栬緫琛� -->
+ <Grid Grid.Row="2" Margin="0,6,0,0">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="80"/>
+ </Grid.ColumnDefinitions>
+ <TextBox Grid.Column="0" x:Name="tbChId" Style="{StaticResource EditorTextBoxStyle}"
+ pu:TextBoxHelper.Watermark="缂栧彿" Margin="0,0,4,0"/>
+ <TextBox Grid.Column="1" x:Name="tbChCap" Style="{StaticResource EditorTextBoxStyle}"
+ pu:TextBoxHelper.Watermark="鑳藉姏鎻忚堪" Margin="0,0,4,0"/>
+ <TextBox Grid.Column="2" x:Name="tbChVolt" Style="{StaticResource EditorTextBoxStyle}"
+ pu:TextBoxHelper.Watermark="鏈�澶х數鍘�" Margin="0,0,4,0"/>
+ <TextBox Grid.Column="3" x:Name="tbChCurr" Style="{StaticResource EditorTextBoxStyle}"
+ pu:TextBoxHelper.Watermark="鏈�澶х數娴�" Margin="0,0,4,0"/>
+ <Button Grid.Column="4" x:Name="saveChBtn" Click="SaveChannel_Click" Content="淇濆瓨"
+ Style="{StaticResource AddBtn}" Width="70"/>
+ </Grid>
+ </Grid>
+ </GroupBox>
+
+ <!-- 搴曢儴鎸夐挳 -->
+ <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10" Margin="0,12,0,0">
+ <Button x:Name="saveBtn" Click="SaveBtn_Click" Content="淇濆瓨"
+ Style="{StaticResource AddBtn}" Width="90"/>
+ <Button x:Name="cancelBtn" Click="CancelBtn_Click" Content="鍙栨秷"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ </StackPanel>
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.DeviceConfig/DeviceEditModal.xaml.cs b/Plugin.DeviceConfig/DeviceEditModal.xaml.cs
new file mode 100644
index 0000000..9113dca
--- /dev/null
+++ b/Plugin.DeviceConfig/DeviceEditModal.xaml.cs
@@ -0,0 +1,368 @@
+using HMI.UI.Util;
+using Panuon.WPF.UI;
+using Plugin.DeviceConfig.Entity;
+using Plugin.DeviceConfig.Model;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Plugin.DeviceConfig
+{
+ /// <summary>
+ /// DeviceEditModal.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class DeviceEditModal : WindowX
+ {
+ private DeviceConfigModel _model;
+ private DeviceDefinition _device;
+ private ObservableCollection<DeviceChannel> _channels = new();
+ private bool _isNew;
+
+ public DeviceEditModal()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 鎵撳紑寮圭獥
+ /// </summary>
+ public bool? Open(DeviceDefinition device, DeviceConfigModel model)
+ {
+ _model = model;
+ _device = device;
+ _isNew = string.IsNullOrEmpty(device.DeviceId);
+
+ // 鍒濆鍖栨灇涓句笅鎷�
+ cbKind.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast<DeviceKind>().ToList();
+ cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast<TransportType>().ToList();
+ cbTransport.SelectionChanged += CbTransport_SelectionChanged;
+
+ // 濉厖鏁版嵁
+ tbDeviceId.Text = device.DeviceId;
+ tbDeviceId.IsReadOnly = !_isNew;
+ cbKind.SelectedItem = device.Kind;
+ tbDriverType.Text = device.DriverType;
+ cbTransport.SelectedItem = device.Transport;
+ tbEndpoint.Text = device.Endpoint;
+
+ // 榛樿椹卞姩绫诲瀷寤鸿 & 绔偣鎻愮ず
+ UpdateDriverHint();
+ UpdateEndpointHint();
+
+ // 鍔犺浇閫氶亾
+ if (!_isNew && !string.IsNullOrEmpty(device.DeviceId))
+ {
+ var chList = _model.GetChannelsByDevice(device.DeviceId);
+ _channels = new ObservableCollection<DeviceChannel>(chList);
+ }
+ else
+ {
+ _channels = new ObservableCollection<DeviceChannel>();
+ }
+ channelListView.ItemsSource = _channels;
+
+ return ShowDialog();
+ }
+
+ #region 閫氶亾鎿嶄綔
+
+ private void AddChannel_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (!int.TryParse(tbChId.Text, out int chId) || chId < 0)
+ {
+ ToastHelper.Warning("璇疯緭鍏ユ湁鏁堢殑閫氶亾缂栧彿锛堥潪璐熸暣鏁帮級");
+ return;
+ }
+ if (_channels.Any(c => c.ChannelId == chId))
+ {
+ ToastHelper.Warning($"閫氶亾 {chId} 宸插瓨鍦�");
+ return;
+ }
+
+ double.TryParse(tbChVolt.Text, out double volt);
+ double.TryParse(tbChCurr.Text, out double curr);
+
+ var ch = new DeviceChannel
+ {
+ DeviceId = _device.DeviceId ?? tbDeviceId.Text,
+ ChannelId = chId,
+ Capability = tbChCap.Text ?? string.Empty,
+ MaxVoltage = volt,
+ MaxCurrent = curr,
+ };
+ _channels.Add(ch);
+ ClearChannelInputs();
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鏂板閫氶亾澶辫触: {ex.Message}");
+ }
+ }
+
+ private void EditChannel_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (sender is Button btn && btn.Tag is DeviceChannel ch)
+ {
+ tbChId.Text = ch.ChannelId.ToString();
+ tbChCap.Text = ch.Capability;
+ tbChVolt.Text = ch.MaxVoltage.ToString("F2");
+ tbChCurr.Text = ch.MaxCurrent.ToString("F2");
+ }
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍔犺浇閫氶亾鏁版嵁澶辫触: {ex.Message}");
+ }
+ }
+
+ private void DeleteChannel_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (sender is Button btn && btn.Tag is DeviceChannel ch)
+ {
+ if (MsgBoxHelper.WarnMessage(this, $"纭畾瑕佸垹闄ら�氶亾 {ch.ChannelId} 鍚楋紵") == MessageBoxResult.OK)
+ {
+ _channels.Remove(ch);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍒犻櫎閫氶亾澶辫触: {ex.Message}");
+ }
+ }
+
+ private void SaveChannel_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (!int.TryParse(tbChId.Text, out int chId) || chId < 0)
+ {
+ ToastHelper.Warning("璇疯緭鍏ユ湁鏁堢殑閫氶亾缂栧彿");
+ return;
+ }
+
+ double.TryParse(tbChVolt.Text, out double volt);
+ double.TryParse(tbChCurr.Text, out double curr);
+
+ var existing = _channels.FirstOrDefault(c => c.ChannelId == chId);
+ if (existing != null)
+ {
+ existing.Capability = tbChCap.Text ?? string.Empty;
+ existing.MaxVoltage = volt;
+ existing.MaxCurrent = curr;
+ }
+ else
+ {
+ _channels.Add(new DeviceChannel
+ {
+ DeviceId = _device.DeviceId ?? tbDeviceId.Text,
+ ChannelId = chId,
+ Capability = tbChCap.Text ?? string.Empty,
+ MaxVoltage = volt,
+ MaxCurrent = curr,
+ });
+ }
+ ClearChannelInputs();
+ ToastHelper.Success("閫氶亾宸蹭繚瀛�");
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"淇濆瓨閫氶亾澶辫触: {ex.Message}");
+ }
+ }
+
+ private void DelChannel_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var selected = channelListView.SelectedItem as DeviceChannel;
+ if (selected == null)
+ {
+ ToastHelper.Warning("璇峰厛閫変腑瑕佸垹闄ょ殑閫氶亾");
+ return;
+ }
+ if (MsgBoxHelper.WarnMessage(this, $"纭畾瑕佸垹闄ら�氶亾 {selected.ChannelId} 鍚楋紵") == MessageBoxResult.OK)
+ {
+ _channels.Remove(selected);
+ }
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍒犻櫎閫氶亾澶辫触: {ex.Message}");
+ }
+ }
+
+ private void ClearChannelInputs()
+ {
+ tbChId.Text = string.Empty;
+ tbChCap.Text = string.Empty;
+ tbChVolt.Text = string.Empty;
+ tbChCurr.Text = string.Empty;
+ }
+
+ #endregion
+
+ #region 淇濆瓨/鍙栨秷
+
+ private void SaveBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // 楠岃瘉
+ var deviceId = tbDeviceId.Text?.Trim();
+ if (string.IsNullOrEmpty(deviceId))
+ {
+ ToastHelper.Warning("璁惧ID涓嶈兘涓虹┖");
+ return;
+ }
+
+ if (cbKind.SelectedItem is not DeviceKind kind)
+ {
+ ToastHelper.Warning("璇烽�夋嫨璁惧绫诲瀷");
+ return;
+ }
+
+ if (cbTransport.SelectedItem is not TransportType transport)
+ {
+ ToastHelper.Warning("璇烽�夋嫨浼犺緭鏂瑰紡");
+ return;
+ }
+
+ var endpoint = tbEndpoint.Text?.Trim();
+ if (string.IsNullOrEmpty(endpoint))
+ {
+ ToastHelper.Warning("杩炴帴绔偣涓嶈兘涓虹┖");
+ return;
+ }
+
+ // 楠岃瘉 Endpoint 鏍煎紡
+ if (transport == TransportType.Tcp)
+ {
+ if (!endpoint.Contains(":") && !endpoint.Contains("."))
+ {
+ ToastHelper.Warning("TCP绔偣鏍煎紡涓嶆纭紝搴斾负 IP:Port 鏍煎紡");
+ return;
+ }
+ }
+ else if (transport == TransportType.Can)
+ {
+ // CAN 绔偣鏍煎紡锛氭�荤嚎鍚嶇О:鑺傜偣缂栧彿 濡� "CAN_BUS_1:0"
+ var parts = endpoint.Split(':');
+ if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[0]) || !int.TryParse(parts[1], out int nodeIdx) || nodeIdx < 0)
+ {
+ ToastHelper.Warning("CAN绔偣鏍煎紡涓嶆纭紝搴斾负 鎬荤嚎鍚嶇О:鑺傜偣缂栧彿锛堝 CAN_BUS_1:0锛�");
+ return;
+ }
+ }
+
+ // 妫�鏌� DeviceId 鏄惁閲嶅锛堟柊澧炴椂锛�
+ if (_isNew)
+ {
+ var existing = _model.GetDeviceById(deviceId);
+ if (existing != null)
+ {
+ ToastHelper.Warning($"璁惧ID [{deviceId}] 宸插瓨鍦�");
+ return;
+ }
+ }
+
+ // 淇濆瓨璁惧
+ _device.DeviceId = deviceId;
+ _device.Kind = kind;
+ _device.DriverType = tbDriverType.Text?.Trim() ?? string.Empty;
+ _device.Transport = transport;
+ _device.Endpoint = endpoint;
+
+ _model.SaveDevice(_device);
+
+ // 淇濆瓨閫氶亾
+ foreach (var ch in _channels)
+ {
+ ch.DeviceId = deviceId;
+ _model.SaveChannel(ch);
+ }
+
+ DialogResult = true;
+ Close();
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"淇濆瓨澶辫触: {ex.Message}");
+ }
+ }
+
+ private void CancelBtn_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void CbTransport_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ UpdateEndpointHint();
+ }
+
+ /// <summary>
+ /// 鏍规嵁浼犺緭鏂瑰紡鏇存柊绔偣杈撳叆妗嗘彁绀�
+ /// </summary>
+ private void UpdateEndpointHint()
+ {
+ if (cbTransport.SelectedItem is TransportType tt)
+ {
+ switch (tt)
+ {
+ case TransportType.Can:
+ Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "CAN鎬荤嚎绔偣锛屾牸寮�: 鎬荤嚎鍚嶇О:鑺傜偣缂栧彿锛堝 CAN_BUS_1:0锛�");
+ break;
+ case TransportType.Serial:
+ Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "涓插彛鍙凤紝濡� COM3:115200:8:N:1");
+ break;
+ case TransportType.Visa:
+ Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "VISG 鍦板潃锛屽 TCPIP0::192.168.1.100::inst0::INSTR");
+ break;
+ case TransportType.SDK:
+ Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "SDK 璁惧鏍囪瘑锛屽 SDK://DeviceName/Instance");
+ break;
+ default:
+ Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "渚�: 192.168.10.124:30000 鎴� COM3");
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// 鏍规嵁璁惧绫诲瀷寤鸿榛樿椹卞姩绫诲瀷
+ /// </summary>
+ private void UpdateDriverHint()
+ {
+ // 淇濈暀鐜版湁椹卞姩绫诲瀷锛屼笉鑷姩淇敼銆備粎褰撲负绌烘椂鎻愮ず榛樿鍊�
+ if (string.IsNullOrWhiteSpace(tbDriverType.Text) && cbKind.SelectedItem is DeviceKind kind)
+ {
+ string hint = kind switch
+ {
+ DeviceKind.CanDevice => "CAN",
+ DeviceKind.PowerSupply => "SCPI",
+ DeviceKind.PlcDevice => "S7",
+ DeviceKind.Chamber => "Tcp",
+ DeviceKind.DAQ => "Tcp",
+ _ => ""
+ };
+ if (!string.IsNullOrEmpty(hint))
+ {
+ tbDriverType.Text = hint;
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugin.DeviceConfig/Entity/DeviceChannel.cs b/Plugin.DeviceConfig/Entity/DeviceChannel.cs
new file mode 100644
index 0000000..2e2df93
--- /dev/null
+++ b/Plugin.DeviceConfig/Entity/DeviceChannel.cs
@@ -0,0 +1,57 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceConfig.Entity
+{
+ /// <summary>
+ /// 璁惧閫氶亾瀹炰綋
+ /// </summary>
+ public partial class DeviceChannel : ObservableObject
+ {
+ /// <summary>
+ /// 鏃犲弬鏋勯�犲嚱鏁帮紙鐢ㄤ簬鏁版嵁缁戝畾锛�
+ /// </summary>
+ public DeviceChannel() { }
+
+ /// <summary>
+ /// 鎵�灞炶澶嘔D锛堣仈鍚堜富閿箣涓�锛�
+ /// </summary>
+ [ObservableProperty]
+ private string deviceId = string.Empty;
+
+ /// <summary>
+ /// 閫氶亾缂栧彿锛堣仈鍚堜富閿箣涓�锛�
+ /// </summary>
+ [ObservableProperty]
+ private int channelId;
+
+ /// <summary>
+ /// 閫氶亾鑳藉姏鎻忚堪
+ /// </summary>
+ [ObservableProperty]
+ private string capability = string.Empty;
+
+ /// <summary>
+ /// 鏈�澶х數鍘�
+ /// </summary>
+ [ObservableProperty]
+ private double maxVoltage;
+
+ /// <summary>
+ /// 鏈�澶х數娴�
+ /// </summary>
+ [ObservableProperty]
+ private double maxCurrent;
+
+ /// <summary>
+ /// 闄愬埗鏉′欢锛圝SON鏍煎紡锛�
+ /// </summary>
+ [ObservableProperty]
+ private string limits = "{}";
+
+ /// <summary>
+ /// 浼氳瘽瀵嗛挜
+ /// </summary>
+ [ObservableProperty]
+ private string sessionKey = string.Empty;
+ }
+}
diff --git a/Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs b/Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs
new file mode 100644
index 0000000..48cd3ba
--- /dev/null
+++ b/Plugin.DeviceConfig/Entity/DeviceConfigVersion.cs
@@ -0,0 +1,62 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceConfig.Entity
+{
+ /// <summary>
+ /// 閰嶇疆鐗堟湰鐘舵�佹灇涓�
+ /// </summary>
+ public enum VersionStatus
+ {
+ Draft,
+ Published,
+ Effective
+ }
+
+ /// <summary>
+ /// 閰嶇疆鐗堟湰瀹炰綋
+ /// </summary>
+ public partial class DeviceConfigVersion : ObservableObject
+ {
+ /// <summary>
+ /// 鏃犲弬鏋勯�犲嚱鏁帮紙鐢ㄤ簬鏁版嵁缁戝畾锛�
+ /// </summary>
+ public DeviceConfigVersion() { }
+
+ /// <summary>
+ /// 鐗堟湰鍞竴鏍囪瘑锛堜富閿級
+ /// </summary>
+ [ObservableProperty]
+ private string versionId = string.Empty;
+
+ /// <summary>
+ /// 鐗堟湰鐘舵��: Draft / Published / Effective
+ /// </summary>
+ [ObservableProperty]
+ private VersionStatus status;
+
+ /// <summary>
+ /// 閰嶇疆鍝堝笇锛圫HA256锛�
+ /// </summary>
+ [ObservableProperty]
+ private string hash = string.Empty;
+
+ /// <summary>
+ /// 浣滆��
+ /// </summary>
+ [ObservableProperty]
+ private string author = string.Empty;
+
+ /// <summary>
+ /// 鏃堕棿鎴�
+ /// </summary>
+ [ObservableProperty]
+ private DateTime timestamp = DateTime.Now;
+
+ /// <summary>
+ /// 宸紓鎻忚堪
+ /// </summary>
+ [ObservableProperty]
+ private string diffDescription = string.Empty;
+ }
+}
diff --git a/Plugin.DeviceConfig/Entity/DeviceDefinition.cs b/Plugin.DeviceConfig/Entity/DeviceDefinition.cs
new file mode 100644
index 0000000..bc15286
--- /dev/null
+++ b/Plugin.DeviceConfig/Entity/DeviceDefinition.cs
@@ -0,0 +1,88 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceConfig.Entity
+{
+ /// <summary>
+ /// 璁惧绉嶇被鏋氫妇
+ /// </summary>
+ public enum DeviceKind
+ {
+ PowerSupply,
+ CanDevice,
+ PlcDevice,
+ Chamber,
+ DAQ
+ }
+
+ /// <summary>
+ /// 浼犺緭鏂瑰紡鏋氫妇
+ /// </summary>
+ public enum TransportType
+ {
+ Tcp,
+ Serial,
+ Visa,
+ SDK,
+ Can
+ }
+
+ /// <summary>
+ /// 璁惧瀹氫箟瀹炰綋
+ /// </summary>
+ public partial class DeviceDefinition : ObservableObject
+ {
+ /// <summary>
+ /// 鏃犲弬鏋勯�犲嚱鏁帮紙鐢ㄤ簬鏁版嵁缁戝畾锛�
+ /// </summary>
+ public DeviceDefinition() { }
+
+ /// <summary>
+ /// 璁惧鍞竴鏍囪瘑锛堜富閿級
+ /// </summary>
+ [ObservableProperty]
+ private string deviceId = string.Empty;
+
+ /// <summary>
+ /// 璁惧绉嶇被: PowerSupply / CanDevice / PlcDevice / Chamber / DAQ
+ /// </summary>
+ [ObservableProperty]
+ private DeviceKind kind;
+
+ /// <summary>
+ /// 椹卞姩绫诲瀷鍚嶇О
+ /// </summary>
+ [ObservableProperty]
+ private string driverType = string.Empty;
+
+ /// <summary>
+ /// 浼犺緭鏂瑰紡: Tcp / Serial / Visa / SDK
+ /// </summary>
+ [ObservableProperty]
+ private TransportType transport;
+
+ /// <summary>
+ /// 杩炴帴绔偣锛圛P:Port 鎴� COM鍙o級
+ /// </summary>
+ [ObservableProperty]
+ private string endpoint = string.Empty;
+
+ /// <summary>
+ /// 鐢熸晥鐗堟湰鍙�
+ /// </summary>
+ [ObservableProperty]
+ private string effectiveVersion = string.Empty;
+
+ /// <summary>
+ /// 鍒涘缓鏃堕棿
+ /// </summary>
+ [ObservableProperty]
+ private DateTime createdAt = DateTime.Now;
+
+ /// <summary>
+ /// 鏇存柊鏃堕棿
+ /// </summary>
+ [ObservableProperty]
+ private DateTime updatedAt = DateTime.Now;
+ }
+}
diff --git a/Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs b/Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs
new file mode 100644
index 0000000..703fc39
--- /dev/null
+++ b/Plugin.DeviceConfig/Entity/DeviceGroupTemplate.cs
@@ -0,0 +1,63 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceConfig.Entity
+{
+ /// <summary>
+ /// 璁惧缁勬ā鏉垮疄浣�
+ /// </summary>
+ public partial class DeviceGroupTemplate : ObservableObject
+ {
+ /// <summary>
+ /// 鏃犲弬鏋勯�犲嚱鏁帮紙鐢ㄤ簬鏁版嵁缁戝畾锛�
+ /// </summary>
+ public DeviceGroupTemplate() { }
+
+ /// <summary>
+ /// 妯℃澘鍞竴鏍囪瘑锛堜富閿級
+ /// </summary>
+ [ObservableProperty]
+ private string templateId = string.Empty;
+
+ /// <summary>
+ /// 璁惧绫诲瀷
+ /// </summary>
+ [ObservableProperty]
+ private string deviceType = string.Empty;
+
+ /// <summary>
+ /// 椹卞姩绫诲瀷
+ /// </summary>
+ [ObservableProperty]
+ private string driverType = string.Empty;
+
+ /// <summary>
+ /// 浼犺緭鏂瑰紡
+ /// </summary>
+ [ObservableProperty]
+ private string transport = string.Empty;
+
+ /// <summary>
+ /// 璁惧鏁伴噺
+ /// </summary>
+ [ObservableProperty]
+ private int deviceCount;
+
+ /// <summary>
+ /// 姣忚澶囬�氶亾鏁�
+ /// </summary>
+ [ObservableProperty]
+ private int channelsPerDevice;
+
+ /// <summary>
+ /// 鍦板潃瑙勫垯锛堝IP妯℃澘 192.168.10.{0}锛�
+ /// </summary>
+ [ObservableProperty]
+ private string addressRule = string.Empty;
+
+ /// <summary>
+ /// 璧峰Slot
+ /// </summary>
+ [ObservableProperty]
+ private int slotStart;
+ }
+}
diff --git a/Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs b/Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs
new file mode 100644
index 0000000..cfbee8b
--- /dev/null
+++ b/Plugin.DeviceConfig/Entity/SlotDeviceMapping.cs
@@ -0,0 +1,51 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceConfig.Entity
+{
+ /// <summary>
+ /// Slot鏄犲皠瀹炰綋
+ /// </summary>
+ public partial class SlotDeviceMapping : ObservableObject
+ {
+ /// <summary>
+ /// 鏃犲弬鏋勯�犲嚱鏁帮紙鐢ㄤ簬鏁版嵁缁戝畾锛�
+ /// </summary>
+ public SlotDeviceMapping() { }
+
+ /// <summary>
+ /// Slot鏍囪瘑锛堣仈鍚堜富閿箣涓�锛�
+ /// </summary>
+ [ObservableProperty]
+ private string slotId = string.Empty;
+
+ /// <summary>
+ /// 鑳藉姏鎻忚堪锛堣仈鍚堜富閿箣涓�锛�
+ /// </summary>
+ [ObservableProperty]
+ private string capability = string.Empty;
+
+ /// <summary>
+ /// 鐗堟湰锛堣仈鍚堜富閿箣涓�锛�
+ /// </summary>
+ [ObservableProperty]
+ private string version = string.Empty;
+
+ /// <summary>
+ /// 鍏宠仈璁惧ID
+ /// </summary>
+ [ObservableProperty]
+ private string deviceId = string.Empty;
+
+ /// <summary>
+ /// 鍏宠仈閫氶亾ID
+ /// </summary>
+ [ObservableProperty]
+ private int channelId;
+
+ /// <summary>
+ /// 鏄犲皠鐗堟湰
+ /// </summary>
+ [ObservableProperty]
+ private string mappingVersion = string.Empty;
+ }
+}
diff --git a/Plugin.DeviceConfig/Model/DeviceConfigModel.cs b/Plugin.DeviceConfig/Model/DeviceConfigModel.cs
new file mode 100644
index 0000000..d679925
--- /dev/null
+++ b/Plugin.DeviceConfig/Model/DeviceConfigModel.cs
@@ -0,0 +1,645 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using HMI.Core.Util;
+using Plugin.DeviceConfig.Entity;
+
+namespace Plugin.DeviceConfig.Model
+{
+ /// <summary>
+ /// 璁惧閰嶇疆鏁版嵁璁块棶灞傦紝鍩轰簬 SQLite 瀹炵幇
+ /// </summary>
+ public class DeviceConfigModel
+ {
+ private string _dbPath = string.Empty;
+
+ /// <summary>
+ /// 鍒濆鍖栨暟鎹簱锛屽垱寤烘墍鏈夎〃
+ /// </summary>
+ /// <param name="dbPath">鏁版嵁搴撴枃浠惰矾寰�</param>
+ public void InitDatabase(string dbPath)
+ {
+ _dbPath = dbPath;
+ try
+ {
+ // 纭繚鐩綍瀛樺湪
+ string? dir = System.IO.Path.GetDirectoryName(dbPath);
+ if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir))
+ {
+ System.IO.Directory.CreateDirectory(dir);
+ }
+
+ // 浠呭湪鏁版嵁搴撴枃浠朵笉瀛樺湪鏃跺垱寤猴紙閬垮厤閲嶅惎鍚庢姏寮傚父鎴栦涪澶卞凡鏈夋暟鎹級
+ if (!System.IO.File.Exists(dbPath))
+ {
+ SqliteHelper.CreateDatabase(dbPath);
+ }
+
+ using var helper = new SqliteHelper(dbPath);
+
+ var sqls = new List<string>
+ {
+ // 璁惧瀹氫箟琛�
+ @"CREATE TABLE IF NOT EXISTS DeviceDefinition (
+ DeviceId TEXT PRIMARY KEY,
+ Kind TEXT NOT NULL,
+ DriverType TEXT NOT NULL,
+ Transport TEXT NOT NULL,
+ Endpoint TEXT NOT NULL,
+ EffectiveVersion TEXT NOT NULL DEFAULT '',
+ CreatedAt TEXT NOT NULL,
+ UpdatedAt TEXT NOT NULL
+ );",
+
+ // 璁惧閫氶亾琛�
+ @"CREATE TABLE IF NOT EXISTS DeviceChannel (
+ DeviceId TEXT NOT NULL,
+ ChannelId INTEGER NOT NULL,
+ Capability TEXT NOT NULL DEFAULT '',
+ MaxVoltage REAL NOT NULL DEFAULT 0,
+ MaxCurrent REAL NOT NULL DEFAULT 0,
+ Limits TEXT NOT NULL DEFAULT '{}',
+ SessionKey TEXT NOT NULL DEFAULT '',
+ PRIMARY KEY (DeviceId, ChannelId)
+ );",
+
+ // Slot鏄犲皠琛�
+ @"CREATE TABLE IF NOT EXISTS SlotDeviceMapping (
+ SlotId TEXT NOT NULL,
+ Capability TEXT NOT NULL,
+ Version TEXT NOT NULL,
+ DeviceId TEXT NOT NULL,
+ ChannelId INTEGER NOT NULL DEFAULT 0,
+ MappingVersion TEXT NOT NULL DEFAULT '',
+ PRIMARY KEY (SlotId, Capability, Version)
+ );",
+
+ // 閰嶇疆鐗堟湰琛�
+ @"CREATE TABLE IF NOT EXISTS DeviceConfigVersion (
+ VersionId TEXT PRIMARY KEY,
+ Status TEXT NOT NULL,
+ Hash TEXT NOT NULL DEFAULT '',
+ Author TEXT NOT NULL DEFAULT '',
+ Timestamp TEXT NOT NULL,
+ DiffDescription TEXT NOT NULL DEFAULT ''
+ );",
+
+ // 璁惧缁勬ā鏉胯〃
+ @"CREATE TABLE IF NOT EXISTS DeviceGroupTemplate (
+ TemplateId TEXT PRIMARY KEY,
+ DeviceType TEXT NOT NULL DEFAULT '',
+ DriverType TEXT NOT NULL DEFAULT '',
+ Transport TEXT NOT NULL DEFAULT '',
+ DeviceCount INTEGER NOT NULL DEFAULT 0,
+ ChannelsPerDevice INTEGER NOT NULL DEFAULT 0,
+ AddressRule TEXT NOT NULL DEFAULT '',
+ SlotStart INTEGER NOT NULL DEFAULT 0
+ );"
+ };
+
+ helper.ExecuteBatchSql(sqls);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鍒濆鍖栬澶囬厤缃暟鎹簱澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ #region DeviceDefinition CRUD
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈夎澶囧畾涔�
+ /// </summary>
+ public List<DeviceDefinition> GetAllDevices()
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable("SELECT * FROM DeviceDefinition ORDER BY CreatedAt DESC;");
+ return MapToDeviceList(dt);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇鎵�鏈夎澶囧畾涔夊け璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏍规嵁ID鑾峰彇璁惧瀹氫箟
+ /// </summary>
+ public DeviceDefinition? GetDeviceById(string deviceId)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable($"SELECT * FROM DeviceDefinition WHERE DeviceId = '{Escape(deviceId)}';");
+ var list = MapToDeviceList(dt);
+ return list.Count > 0 ? list[0] : null;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇璁惧瀹氫箟[{deviceId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 淇濆瓨璁惧瀹氫箟锛堟柊澧炴垨鏇存柊锛�
+ /// </summary>
+ public void SaveDevice(DeviceDefinition device)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ device.UpdatedAt = DateTime.Now;
+
+ var sql = $@"INSERT OR REPLACE INTO DeviceDefinition
+ (DeviceId, Kind, DriverType, Transport, Endpoint, EffectiveVersion, CreatedAt, UpdatedAt)
+ VALUES (
+ '{Escape(device.DeviceId)}',
+ '{device.Kind}',
+ '{Escape(device.DriverType)}',
+ '{device.Transport}',
+ '{Escape(device.Endpoint)}',
+ '{Escape(device.EffectiveVersion)}',
+ '{device.CreatedAt:yyyy-MM-dd HH:mm:ss}',
+ '{device.UpdatedAt:yyyy-MM-dd HH:mm:ss}'
+ );";
+ helper.ExecuteSql(sql);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"淇濆瓨璁惧瀹氫箟[{device.DeviceId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鍒犻櫎璁惧瀹氫箟
+ /// </summary>
+ public void DeleteDevice(string deviceId)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ helper.ExecuteSql($"DELETE FROM DeviceDefinition WHERE DeviceId = '{Escape(deviceId)}';");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鍒犻櫎璁惧瀹氫箟[{deviceId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ private List<DeviceDefinition> MapToDeviceList(DataTable dt)
+ {
+ var list = new List<DeviceDefinition>();
+ foreach (DataRow row in dt.Rows)
+ {
+ list.Add(new DeviceDefinition
+ {
+ DeviceId = row["DeviceId"].ToString() ?? string.Empty,
+ Kind = Enum.TryParse<DeviceKind>(row["Kind"].ToString(), out var kind) ? kind : DeviceKind.PowerSupply,
+ DriverType = row["DriverType"].ToString() ?? string.Empty,
+ Transport = Enum.TryParse<TransportType>(row["Transport"].ToString(), out var tp) ? tp : TransportType.Tcp,
+ Endpoint = row["Endpoint"].ToString() ?? string.Empty,
+ EffectiveVersion = row["EffectiveVersion"].ToString() ?? string.Empty,
+ CreatedAt = DateTime.TryParse(row["CreatedAt"].ToString(), out var ca) ? ca : DateTime.Now,
+ UpdatedAt = DateTime.TryParse(row["UpdatedAt"].ToString(), out var ua) ? ua : DateTime.Now,
+ });
+ }
+ return list;
+ }
+
+ #endregion
+
+ #region DeviceChannel CRUD
+
+ /// <summary>
+ /// 鑾峰彇鎸囧畾璁惧鐨勬墍鏈夐�氶亾
+ /// </summary>
+ public List<DeviceChannel> GetChannelsByDevice(string deviceId)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable($"SELECT * FROM DeviceChannel WHERE DeviceId = '{Escape(deviceId)}' ORDER BY ChannelId;");
+ return MapToChannelList(dt);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇璁惧[{deviceId}]閫氶亾澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 淇濆瓨璁惧閫氶亾锛堟柊澧炴垨鏇存柊锛�
+ /// </summary>
+ public void SaveChannel(DeviceChannel channel)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var sql = $@"INSERT OR REPLACE INTO DeviceChannel
+ (DeviceId, ChannelId, Capability, MaxVoltage, MaxCurrent, Limits, SessionKey)
+ VALUES (
+ '{Escape(channel.DeviceId)}',
+ {channel.ChannelId},
+ '{Escape(channel.Capability)}',
+ {channel.MaxVoltage},
+ {channel.MaxCurrent},
+ '{Escape(channel.Limits)}',
+ '{Escape(channel.SessionKey)}'
+ );";
+ helper.ExecuteSql(sql);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"淇濆瓨閫氶亾[{channel.DeviceId}:{channel.ChannelId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鍒犻櫎璁惧閫氶亾
+ /// </summary>
+ public void DeleteChannel(string deviceId, int channelId)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ helper.ExecuteSql($"DELETE FROM DeviceChannel WHERE DeviceId = '{Escape(deviceId)}' AND ChannelId = {channelId};");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鍒犻櫎閫氶亾[{deviceId}:{channelId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ private List<DeviceChannel> MapToChannelList(DataTable dt)
+ {
+ var list = new List<DeviceChannel>();
+ foreach (DataRow row in dt.Rows)
+ {
+ list.Add(new DeviceChannel
+ {
+ DeviceId = row["DeviceId"].ToString() ?? string.Empty,
+ ChannelId = Convert.ToInt32(row["ChannelId"]),
+ Capability = row["Capability"].ToString() ?? string.Empty,
+ MaxVoltage = Convert.ToDouble(row["MaxVoltage"]),
+ MaxCurrent = Convert.ToDouble(row["MaxCurrent"]),
+ Limits = row["Limits"].ToString() ?? "{}",
+ SessionKey = row["SessionKey"].ToString() ?? string.Empty,
+ });
+ }
+ return list;
+ }
+
+ #endregion
+
+ #region SlotDeviceMapping CRUD
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈塖lot鏄犲皠
+ /// </summary>
+ public List<SlotDeviceMapping> GetSlotMappings()
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable("SELECT * FROM SlotDeviceMapping ORDER BY SlotId;");
+ return MapToSlotMappingList(dt);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇Slot鏄犲皠澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 淇濆瓨Slot鏄犲皠锛堟柊澧炴垨鏇存柊锛�
+ /// </summary>
+ public void SaveSlotMapping(SlotDeviceMapping mapping)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var sql = $@"INSERT OR REPLACE INTO SlotDeviceMapping
+ (SlotId, Capability, Version, DeviceId, ChannelId, MappingVersion)
+ VALUES (
+ '{Escape(mapping.SlotId)}',
+ '{Escape(mapping.Capability)}',
+ '{Escape(mapping.Version)}',
+ '{Escape(mapping.DeviceId)}',
+ {mapping.ChannelId},
+ '{Escape(mapping.MappingVersion)}'
+ );";
+ helper.ExecuteSql(sql);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"淇濆瓨Slot鏄犲皠[{mapping.SlotId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鍒犻櫎Slot鏄犲皠
+ /// </summary>
+ public void DeleteSlotMapping(string slotId, string capability, string version)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ helper.ExecuteSql($@"DELETE FROM SlotDeviceMapping
+ WHERE SlotId = '{Escape(slotId)}'
+ AND Capability = '{Escape(capability)}'
+ AND Version = '{Escape(version)}';");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鍒犻櫎Slot鏄犲皠[{slotId}:{capability}:{version}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ private List<SlotDeviceMapping> MapToSlotMappingList(DataTable dt)
+ {
+ var list = new List<SlotDeviceMapping>();
+ foreach (DataRow row in dt.Rows)
+ {
+ list.Add(new SlotDeviceMapping
+ {
+ SlotId = row["SlotId"].ToString() ?? string.Empty,
+ Capability = row["Capability"].ToString() ?? string.Empty,
+ Version = row["Version"].ToString() ?? string.Empty,
+ DeviceId = row["DeviceId"].ToString() ?? string.Empty,
+ ChannelId = Convert.ToInt32(row["ChannelId"]),
+ MappingVersion = row["MappingVersion"].ToString() ?? string.Empty,
+ });
+ }
+ return list;
+ }
+
+ #endregion
+
+ #region DeviceConfigVersion CRUD
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈夐厤缃増鏈�
+ /// </summary>
+ public List<DeviceConfigVersion> GetConfigVersions()
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable("SELECT * FROM DeviceConfigVersion ORDER BY Timestamp DESC;");
+ return MapToVersionList(dt);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇閰嶇疆鐗堟湰澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 淇濆瓨閰嶇疆鐗堟湰锛堟柊澧炴垨鏇存柊锛�
+ /// </summary>
+ public void SaveConfigVersion(DeviceConfigVersion version)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var sql = $@"INSERT OR REPLACE INTO DeviceConfigVersion
+ (VersionId, Status, Hash, Author, Timestamp, DiffDescription)
+ VALUES (
+ '{Escape(version.VersionId)}',
+ '{version.Status}',
+ '{Escape(version.Hash)}',
+ '{Escape(version.Author)}',
+ '{version.Timestamp:yyyy-MM-dd HH:mm:ss}',
+ '{Escape(version.DiffDescription)}'
+ );";
+ helper.ExecuteSql(sql);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"淇濆瓨閰嶇疆鐗堟湰[{version.VersionId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊鐗堟湰鐘舵��
+ /// </summary>
+ public void UpdateVersionStatus(string versionId, string newStatus)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ helper.ExecuteSql($@"UPDATE DeviceConfigVersion
+ SET Status = '{Escape(newStatus)}'
+ WHERE VersionId = '{Escape(versionId)}';");
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鏇存柊鐗堟湰鐘舵�乕{versionId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ private List<DeviceConfigVersion> MapToVersionList(DataTable dt)
+ {
+ var list = new List<DeviceConfigVersion>();
+ foreach (DataRow row in dt.Rows)
+ {
+ list.Add(new DeviceConfigVersion
+ {
+ VersionId = row["VersionId"].ToString() ?? string.Empty,
+ Status = Enum.TryParse<VersionStatus>(row["Status"].ToString(), out var st) ? st : VersionStatus.Draft,
+ Hash = row["Hash"].ToString() ?? string.Empty,
+ Author = row["Author"].ToString() ?? string.Empty,
+ Timestamp = DateTime.TryParse(row["Timestamp"].ToString(), out var ts) ? ts : DateTime.Now,
+ DiffDescription = row["DiffDescription"].ToString() ?? string.Empty,
+ });
+ }
+ return list;
+ }
+
+ #endregion
+
+ #region DeviceGroupTemplate CRUD
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈夎澶囩粍妯℃澘
+ /// </summary>
+ public List<DeviceGroupTemplate> GetGroupTemplates()
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var dt = helper.GetDataTable("SELECT * FROM DeviceGroupTemplate ORDER BY DeviceType;");
+ return MapToTemplateList(dt);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鑾峰彇璁惧缁勬ā鏉垮け璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 淇濆瓨璁惧缁勬ā鏉匡紙鏂板鎴栨洿鏂帮級
+ /// </summary>
+ public void SaveGroupTemplate(DeviceGroupTemplate template)
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+ var sql = $@"INSERT OR REPLACE INTO DeviceGroupTemplate
+ (TemplateId, DeviceType, DriverType, Transport, DeviceCount, ChannelsPerDevice, AddressRule, SlotStart)
+ VALUES (
+ '{Escape(template.TemplateId)}',
+ '{Escape(template.DeviceType)}',
+ '{Escape(template.DriverType)}',
+ '{Escape(template.Transport)}',
+ {template.DeviceCount},
+ {template.ChannelsPerDevice},
+ '{Escape(template.AddressRule)}',
+ {template.SlotStart}
+ );";
+ helper.ExecuteSql(sql);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"淇濆瓨璁惧缁勬ā鏉縖{template.TemplateId}]澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ private List<DeviceGroupTemplate> MapToTemplateList(DataTable dt)
+ {
+ var list = new List<DeviceGroupTemplate>();
+ foreach (DataRow row in dt.Rows)
+ {
+ list.Add(new DeviceGroupTemplate
+ {
+ TemplateId = row["TemplateId"].ToString() ?? string.Empty,
+ DeviceType = row["DeviceType"].ToString() ?? string.Empty,
+ DriverType = row["DriverType"].ToString() ?? string.Empty,
+ Transport = row["Transport"].ToString() ?? string.Empty,
+ DeviceCount = Convert.ToInt32(row["DeviceCount"]),
+ ChannelsPerDevice = Convert.ToInt32(row["ChannelsPerDevice"]),
+ AddressRule = row["AddressRule"].ToString() ?? string.Empty,
+ SlotStart = Convert.ToInt32(row["SlotStart"]),
+ });
+ }
+ return list;
+ }
+
+ #endregion
+
+ #region 鍐茬獊妫�娴嬩笌鍝堝笇璁$畻
+
+ /// <summary>
+ /// 妫�娴嬮厤缃啿绐侊細IP鍐茬獊銆丼lot鏄犲皠閲嶅銆侀�氶亾瓒婄晫绛�
+ /// </summary>
+ /// <returns>鍐茬獊鎻忚堪鍒楄〃锛屼负绌哄垯琛ㄧず鏃犲啿绐�</returns>
+ public List<string> DetectConflicts()
+ {
+ var conflicts = new List<string>();
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+
+ // 1. 妫�娴婭P/Endpoint鍐茬獊锛堢浉鍚孍ndpoint鐨勪笉鍚岃澶囷級
+ var ipSql = @"SELECT Endpoint, COUNT(*) as Cnt
+ FROM DeviceDefinition
+ WHERE Transport = 'Tcp'
+ GROUP BY Endpoint
+ HAVING Cnt > 1;";
+ var ipDt = helper.GetDataTable(ipSql);
+ foreach (DataRow row in ipDt.Rows)
+ {
+ conflicts.Add($"IP/Endpoint鍐茬獊: {row["Endpoint"]} 琚� {row["Cnt"]} 涓澶囦娇鐢�");
+ }
+
+ // 2. 妫�娴婼lot鏄犲皠閲嶅锛堢浉鍚孲lot+Capability+Version鏄犲皠鍒颁笉鍚岃澶囷級
+ var slotSql = @"SELECT SlotId, Capability, Version, COUNT(*) as Cnt
+ FROM SlotDeviceMapping
+ GROUP BY SlotId, Capability, Version
+ HAVING Cnt > 1;";
+ var slotDt = helper.GetDataTable(slotSql);
+ foreach (DataRow row in slotDt.Rows)
+ {
+ conflicts.Add($"Slot鏄犲皠閲嶅: Slot={row["SlotId"]}, Capability={row["Capability"]}, Version={row["Version"]}");
+ }
+
+ // 3. 妫�娴嬮�氶亾瓒婄晫锛圡axVoltage鎴朚axCurrent涓鸿礋鏁帮級
+ var channelSql = @"SELECT DeviceId, ChannelId, MaxVoltage, MaxCurrent
+ FROM DeviceChannel
+ WHERE MaxVoltage < 0 OR MaxCurrent < 0;";
+ var channelDt = helper.GetDataTable(channelSql);
+ foreach (DataRow row in channelDt.Rows)
+ {
+ conflicts.Add($"閫氶亾鍙傛暟瓒婄晫: 璁惧={row["DeviceId"]}, 閫氶亾={row["ChannelId"]}, " +
+ $"鐢靛帇={row["MaxVoltage"]}, 鐢垫祦={row["MaxCurrent"]}");
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"鍐茬獊妫�娴嬪け璐�: {ex.Message}", ex);
+ }
+
+ return conflicts;
+ }
+
+ /// <summary>
+ /// 璁$畻褰撳墠閰嶇疆鐨凷HA256鍝堝笇
+ /// </summary>
+ /// <returns>SHA256鍝堝笇瀛楃涓诧紙灏忓啓鍗佸叚杩涘埗锛�</returns>
+ public string ComputeConfigHash()
+ {
+ try
+ {
+ using var helper = new SqliteHelper(_dbPath);
+
+ // 鎷兼帴鎵�鏈夎澶囧畾涔夌殑鍏抽敭淇℃伅浣滀负鍝堝笇杈撳叆
+ var sb = new StringBuilder();
+
+ var deviceDt = helper.GetDataTable("SELECT * FROM DeviceDefinition ORDER BY DeviceId;");
+ foreach (DataRow row in deviceDt.Rows)
+ {
+ sb.Append($"DEVICE:{row["DeviceId"]}|{row["Kind"]}|{row["DriverType"]}|{row["Transport"]}|{row["Endpoint"]};");
+ }
+
+ var channelDt = helper.GetDataTable("SELECT * FROM DeviceChannel ORDER BY DeviceId, ChannelId;");
+ foreach (DataRow row in channelDt.Rows)
+ {
+ sb.Append($"CHANNEL:{row["DeviceId"]}|{row["ChannelId"]}|{row["Capability"]}|{row["MaxVoltage"]}|{row["MaxCurrent"]};");
+ }
+
+ var slotDt = helper.GetDataTable("SELECT * FROM SlotDeviceMapping ORDER BY SlotId, Capability, Version;");
+ foreach (DataRow row in slotDt.Rows)
+ {
+ sb.Append($"SLOT:{row["SlotId"]}|{row["Capability"]}|{row["Version"]}|{row["DeviceId"]}|{row["ChannelId"]};");
+ }
+
+ using var sha256 = SHA256.Create();
+ var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString()));
+ return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"璁$畻閰嶇疆鍝堝笇澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ #endregion
+
+ #region 杈呭姪鏂规硶
+
+ /// <summary>
+ /// SQL瀛楃涓茶浆涔夛紙闃叉娉ㄥ叆锛�
+ /// </summary>
+ private static string Escape(string value)
+ {
+ return value?.Replace("'", "''") ?? string.Empty;
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugin.DeviceConfig/Plugin.DeviceConfig.csproj b/Plugin.DeviceConfig/Plugin.DeviceConfig.csproj
new file mode 100644
index 0000000..b444e17
--- /dev/null
+++ b/Plugin.DeviceConfig/Plugin.DeviceConfig.csproj
@@ -0,0 +1,27 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ <PackageReference Include="log4net" Version="3.0.4" />
+ <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+ <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" />
+ <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
+ <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
+ <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugin.DeviceConfig/VersionManageModal.xaml b/Plugin.DeviceConfig/VersionManageModal.xaml
new file mode 100644
index 0000000..5db25e1
--- /dev/null
+++ b/Plugin.DeviceConfig/VersionManageModal.xaml
@@ -0,0 +1,166 @@
+<pu:WindowX x:Class="Plugin.DeviceConfig.VersionManageModal"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:local="clr-namespace:Plugin.DeviceConfig"
+ mc:Ignorable="d"
+ Title="鐗堟湰绠$悊"
+ Height="520"
+ Width="800"
+ WindowStartupLocation="CenterOwner"
+ Style="{StaticResource ModalWindow}">
+ <pu:WindowX.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </pu:WindowX.Resources>
+
+ <Grid Margin="16">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 鎿嶄綔鎸夐挳鏍� -->
+ <StackPanel Grid.Row="0" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10" Margin="0,0,0,8">
+ <Button x:Name="publishBtn" Click="PublishBtn_Click" Content="鍙戝竷閫変腑鐗堟湰"
+ Style="{StaticResource AddBtn}" Width="110"/>
+ <Button x:Name="applyBtn" Click="ApplyBtn_Click" Content="搴旂敤閫変腑鐗堟湰"
+ Style="{StaticResource AddBtn}" Width="110"/>
+ <Button x:Name="detailBtn" Click="DetailBtn_Click" Content="鐗堟湰璇︽儏"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ <Button x:Name="rfhBtn" Click="RfhBtn_Click" Content="鍒锋柊"
+ Style="{StaticResource RfhBtn}" Width="70"/>
+ </StackPanel>
+
+ <!-- 鐗堟湰鍒楄〃 -->
+ <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="versionListView"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="50" Header="#">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Converter={StaticResource lvRowNumber},
+ RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="180" Header="VersionId">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding VersionId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="Status">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <StackPanel Orientation="Horizontal">
+ <Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0"
+ VerticalAlignment="Center">
+ <Border.Style>
+ <Style TargetType="Border">
+ <Setter Property="Background" Value="Gray"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="Draft">
+ <Setter Property="Background" Value="Gray"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Published">
+ <Setter Property="Background" Value="#2196F3"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Effective">
+ <Setter Property="Background" Value="#4CAF50"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Border.Style>
+ </Border>
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Status}" VerticalAlignment="Center"/>
+ </StackPanel>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="200" Header="Hash">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding HashShort}"
+ ToolTip="{Binding Hash}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="Author">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Author}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="160" Header="Timestamp">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Timestamp, StringFormat=yyyy-MM-dd HH:mm:ss}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+
+ <!-- 鐗堟湰璇︽儏鍖� -->
+ <Border Grid.Row="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4"
+ Margin="0,8,0,0" Padding="10" x:Name="detailPanel" Visibility="Collapsed">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="瀹屾暣Hash:" FontWeight="SemiBold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="0" Grid.Column="1" x:Name="tbDetailHash" TextWrapping="Wrap"
+ FontFamily="Consolas" Margin="4,2"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="宸紓鎻忚堪:" FontWeight="SemiBold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="1" Grid.Column="1" x:Name="tbDetailDiff" TextWrapping="Wrap" Margin="4,2"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="浣滆��:" FontWeight="SemiBold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="2" Grid.Column="1" x:Name="tbDetailAuthor" Margin="4,2"/>
+ </Grid>
+ </Border>
+
+ <!-- 搴曢儴鎸夐挳 -->
+ <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
+ <Button x:Name="closeBtn" Click="CloseBtn_Click" Content="鍏抽棴"
+ Style="{StaticResource RfhBtn}" Width="90"/>
+ </StackPanel>
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.DeviceConfig/VersionManageModal.xaml.cs b/Plugin.DeviceConfig/VersionManageModal.xaml.cs
new file mode 100644
index 0000000..ea2c2fd
--- /dev/null
+++ b/Plugin.DeviceConfig/VersionManageModal.xaml.cs
@@ -0,0 +1,207 @@
+using HMI.UI.Util;
+using Panuon.WPF.UI;
+using Plugin.DeviceConfig.Entity;
+using Plugin.DeviceConfig.Model;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows;
+
+namespace Plugin.DeviceConfig
+{
+ /// <summary>
+ /// VersionManageModal.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class VersionManageModal : WindowX
+ {
+ private DeviceConfigModel _model;
+ private ObservableCollection<VersionDisplayItem> _versions = new();
+ private bool _changed;
+
+ public VersionManageModal()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 鎵撳紑寮圭獥
+ /// </summary>
+ public bool? Open(DeviceConfigModel model)
+ {
+ _model = model;
+ versionListView.ItemsSource = _versions;
+ LoadVersions();
+ return ShowDialog();
+ }
+
+ private void LoadVersions()
+ {
+ try
+ {
+ var list = _model.GetConfigVersions();
+ _versions.Clear();
+ foreach (var v in list)
+ {
+ _versions.Add(new VersionDisplayItem
+ {
+ VersionId = v.VersionId,
+ Status = v.Status,
+ Hash = v.Hash,
+ HashShort = v.Hash.Length > 16 ? v.Hash.Substring(0, 16) + "..." : v.Hash,
+ Author = v.Author,
+ Timestamp = v.Timestamp,
+ DiffDescription = v.DiffDescription,
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍔犺浇鐗堟湰鍒楄〃澶辫触: {ex.Message}");
+ }
+ }
+
+ #region 鎿嶄綔
+
+ private void PublishBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var selected = versionListView.SelectedItem as VersionDisplayItem;
+ if (selected == null)
+ {
+ ToastHelper.Warning("璇峰厛閫変腑涓�涓増鏈�");
+ return;
+ }
+ if (selected.Status != VersionStatus.Draft)
+ {
+ ToastHelper.Warning("鍙湁 Draft 鐘舵�佺殑鐗堟湰鍙互鍙戝竷");
+ return;
+ }
+
+ if (MsgBoxHelper.WarnMessage(this, $"纭畾瑕佸彂甯冪増鏈� [{selected.VersionId}] 鍚楋紵") != MessageBoxResult.OK)
+ return;
+
+ // 璁$畻鏈�鏂� Hash
+ var hash = _model.ComputeConfigHash();
+ selected.Hash = hash;
+ selected.HashShort = hash.Length > 16 ? hash.Substring(0, 16) + "..." : hash;
+ selected.Status = VersionStatus.Published;
+ selected.Timestamp = DateTime.Now;
+
+ var ver = new DeviceConfigVersion
+ {
+ VersionId = selected.VersionId,
+ Status = VersionStatus.Published,
+ Hash = hash,
+ Author = selected.Author,
+ Timestamp = DateTime.Now,
+ DiffDescription = selected.DiffDescription,
+ };
+ _model.SaveConfigVersion(ver);
+
+ _changed = true;
+ LoadVersions();
+ ToastHelper.Success($"鐗堟湰 [{selected.VersionId}] 宸插彂甯�");
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍙戝竷澶辫触: {ex.Message}");
+ }
+ }
+
+ private void ApplyBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var selected = versionListView.SelectedItem as VersionDisplayItem;
+ if (selected == null)
+ {
+ ToastHelper.Warning("璇峰厛閫変腑涓�涓増鏈�");
+ return;
+ }
+ if (selected.Status != VersionStatus.Published)
+ {
+ ToastHelper.Warning("鍙湁 Published 鐘舵�佺殑鐗堟湰鍙互搴旂敤");
+ return;
+ }
+
+ // 妫�鏌ュ綋鍓� Effective 鐗堟湰
+ var currentEffective = _versions.FirstOrDefault(v => v.Status == VersionStatus.Effective);
+ if (currentEffective != null)
+ {
+ if (MsgBoxHelper.WarnMessage(this,
+ $"褰撳墠宸叉湁鐢熸晥鐗堟湰 [{currentEffective.VersionId}]锛屽簲鐢ㄦ柊鐗堟湰灏嗘浛鎹㈠畠銆俓n鏄惁缁х画锛�") != MessageBoxResult.OK)
+ {
+ return;
+ }
+
+ // 灏嗗師 Effective 鏀瑰洖 Published
+ _model.UpdateVersionStatus(currentEffective.VersionId, VersionStatus.Published.ToString());
+ }
+
+ if (MsgBoxHelper.WarnMessage(this, $"纭畾瑕佸皢 [{selected.VersionId}] 璁句负鐢熸晥鐗堟湰鍚楋紵") != MessageBoxResult.OK)
+ return;
+
+ _model.UpdateVersionStatus(selected.VersionId, VersionStatus.Effective.ToString());
+ _changed = true;
+ LoadVersions();
+ ToastHelper.Success($"鐗堟湰 [{selected.VersionId}] 宸茶涓虹敓鏁堢増鏈�");
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"搴旂敤鐗堟湰澶辫触: {ex.Message}");
+ }
+ }
+
+ private void DetailBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var selected = versionListView.SelectedItem as VersionDisplayItem;
+ if (selected == null)
+ {
+ ToastHelper.Warning("璇峰厛閫変腑涓�涓増鏈�");
+ return;
+ }
+
+ tbDetailHash.Text = selected.Hash;
+ tbDetailDiff.Text = selected.DiffDescription;
+ tbDetailAuthor.Text = $"{selected.Author} @ {selected.Timestamp:yyyy-MM-dd HH:mm:ss}";
+ detailPanel.Visibility = Visibility.Visible;
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Error($"鍔犺浇璇︽儏澶辫触: {ex.Message}");
+ }
+ }
+
+ private void RfhBtn_Click(object sender, RoutedEventArgs e)
+ {
+ LoadVersions();
+ ToastHelper.Success("宸插埛鏂�");
+ }
+
+ private void CloseBtn_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = _changed;
+ Close();
+ }
+
+ #endregion
+ }
+
+ /// <summary>
+ /// 鐗堟湰鏄剧ず椤�
+ /// </summary>
+ public class VersionDisplayItem
+ {
+ public string VersionId { get; set; } = string.Empty;
+ public VersionStatus Status { get; set; }
+ public string Hash { get; set; } = string.Empty;
+ public string HashShort { get; set; } = string.Empty;
+ public string Author { get; set; } = string.Empty;
+ public DateTime Timestamp { get; set; }
+ public string DiffDescription { get; set; } = string.Empty;
+ }
+}
diff --git a/Plugin.DeviceDebug/DeviceDebug.xaml b/Plugin.DeviceDebug/DeviceDebug.xaml
new file mode 100644
index 0000000..6de3a4b
--- /dev/null
+++ b/Plugin.DeviceDebug/DeviceDebug.xaml
@@ -0,0 +1,177 @@
+<hb:SimpleMaskControl x:Class="Plugin.DeviceDebug.DeviceDebug"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
+ xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:local="clr-namespace:Plugin.DeviceDebug"
+ xmlns:panels="clr-namespace:Plugin.DeviceDebug.Panels"
+ mc:Ignorable="d"
+ Loaded="SimpleMaskControl_Loaded"
+ d:DesignHeight="600" d:DesignWidth="1000">
+ <hb:SimpleMaskControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ <Style x:Key="TitleBox" TargetType="TextBlock">
+ <Setter Property="FontSize" Value="14"/>
+ <Setter Property="FontWeight" Value="SemiBold"/>
+ <Setter Property="Padding" Value="8,4"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ </Style>
+ <BooleanToVisibilityConverter x:Key="BoolToVis"/>
+ </ResourceDictionary>
+ </hb:SimpleMaskControl.Resources>
+
+ <Grid dp:GridHelper.Space="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 椤堕儴鏍囬鏍� -->
+ <DockPanel Grid.Row="0">
+ <StackPanel dp:StackPanelHelper.ChildSpace="10" DockPanel.Dock="Right" Orientation="Horizontal">
+ <ComboBox x:Name="deviceSelector" Width="240"
+ ItemsSource="{Binding DeviceList}"
+ SelectedItem="{Binding SelectedDevice}"
+ SelectionChanged="DeviceSelector_SelectionChanged"
+ VerticalContentAlignment="Center" Padding="6,4"/>
+ <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="RfhBtn_Click"/>
+ </StackPanel>
+ <TextBlock Style="{StaticResource TitleBox}" Text="纭欢璋冭瘯"/>
+ </DockPanel>
+
+ <!-- 璁惧淇℃伅鏍� -->
+ <Border Grid.Row="1" BorderBrush="#CCCCCC" BorderThickness="1" Padding="10,6" Background="#F9F9F9"
+ Visibility="{Binding HasDevice, Converter={StaticResource BoolToVis}}">
+ <WrapPanel dp:StackPanelHelper.ChildSpace="20">
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="璁惧ID:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding CurrentDeviceId}" Style="{StaticResource ListTableTextBlock}"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="鍨嬪彿:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding CurrentDeviceKind}" Style="{StaticResource ListTableTextBlock}"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="杩炴帴鐘舵��:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding ConnectionStateText}" Foreground="{Binding ConnectionBrush}"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="鐗堟湰:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding CurrentEffectiveVersion}" Style="{StaticResource ListTableTextBlock}"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="棰勭害鐘舵��:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding ReservationStatusText}" Style="{StaticResource ListTableTextBlock}"/>
+ </StackPanel>
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4">
+ <TextBlock Text="浼氳瘽鐘舵��:" FontWeight="Bold" Foreground="#555"/>
+ <TextBlock Text="{Binding SessionStateText}" Foreground="{Binding SessionStateBrush}" FontWeight="Bold"/>
+ </StackPanel>
+ </WrapPanel>
+ </Border>
+
+ <!-- 鎿嶄綔鎸夐挳鏍� -->
+ <StackPanel Grid.Row="2" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10"
+ Visibility="{Binding HasDevice, Converter={StaticResource BoolToVis}}">
+ <Button x:Name="enterMaintenanceBtn" Content="杩涘叆缁存姢鎺у埗" Padding="14,6"
+ IsEnabled="{Binding CanEnterMaintenance}"
+ Click="EnterMaintenanceBtn_Click"
+ Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ <Button x:Name="exitMaintenanceBtn" Content="閫�鍑虹淮鎶�" Padding="14,6"
+ IsEnabled="{Binding CanExitMaintenance}"
+ Click="ExitMaintenanceBtn_Click"
+ Background="#FF5722" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ </StackPanel>
+
+ <!-- 涓讳綋鍖哄煙 -->
+ <Grid Grid.Row="3">
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="350"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 宸︿晶闈㈡澘瀹瑰櫒 -->
+ <Border Grid.Column="0" BorderBrush="#DDDDDD" BorderThickness="1" Margin="0,0,8,0">
+ <ContentControl x:Name="panelContainer">
+ <TextBlock Text="璇烽�夋嫨璁惧" HorizontalAlignment="Center" VerticalAlignment="Center"
+ FontSize="16" Foreground="#999"/>
+ </ContentControl>
+ </Border>
+
+ <!-- 鍙充晶鎿嶄綔鏃ュ織 -->
+ <Border Grid.Column="1" BorderBrush="#DDDDDD" BorderThickness="1">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <Border Grid.Row="0" Background="#F0F0F0" Padding="8,6">
+ <TextBlock Text="鎿嶄綔鏃ュ織" FontWeight="Bold" FontSize="14"/>
+ </Border>
+ <ListView Grid.Row="1" x:Name="operationLogList"
+ ItemsSource="{Binding OperationLogs}"
+ Style="{StaticResource DefaultListTable}"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="60" Header="绫诲瀷">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding CommandType}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="120" Header="鍙傛暟">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Parameters}" TextTrimming="CharacterEllipsis"
+ ToolTip="{Binding Parameters}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="60" Header="缁撴灉">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Outcome}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="鏃堕棿">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Timestamp, StringFormat=HH:mm:ss}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+ </Grid>
+ </Grid>
+</hb:SimpleMaskControl>
diff --git a/Plugin.DeviceDebug/DeviceDebug.xaml.cs b/Plugin.DeviceDebug/DeviceDebug.xaml.cs
new file mode 100644
index 0000000..dc82505
--- /dev/null
+++ b/Plugin.DeviceDebug/DeviceDebug.xaml.cs
@@ -0,0 +1,668 @@
+using Aging.Infrastructure.Contracts;
+using CommunityToolkit.Mvvm.ComponentModel;
+using HMI.Base;
+using HMI.Core.Util;
+using HMI.UI.Util;
+using log4net;
+using Plugin.DeviceDebug.Entity;
+using Plugin.DeviceDebug.Model;
+using Plugin.DeviceDebug.Panels;
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace Plugin.DeviceDebug
+{
+ /// <summary>
+ /// DeviceDebug.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ [ObservableObject]
+ public partial class DeviceDebug : SimpleMaskControl
+ {
+ private static readonly ILog _log = LogManager.GetLogger(typeof(DeviceDebug));
+ private DeviceDebugModel _debugModel;
+ private DebugSessionManager _sessionManager;
+
+ // 鍩虹璁炬柦灞傛湇鍔�
+ private IDeviceManager? _deviceManager;
+ private IDeviceService? _deviceService;
+ private IResourceManager? _resourceManager;
+
+ // 璁惧閰嶇疆鏁版嵁搴撹矾寰勶紙涓� DeviceConfig 鎻掍欢鍏变韩锛�
+ private string _deviceConfigDbPath;
+
+ [ObservableProperty] private ObservableCollection<string> _deviceList = new();
+ [ObservableProperty] private string _selectedDevice;
+ [ObservableProperty] private DebugSession _currentSession;
+ [ObservableProperty] private ObservableCollection<DebugOperation> _operationLogs = new();
+ [ObservableProperty] private string _connectionStateText = "鏈繛鎺�";
+ [ObservableProperty] private string _sessionStateText = "";
+ [ObservableProperty] private string _reservationStatusText = "";
+ [ObservableProperty] private string _currentDeviceId = "";
+ [ObservableProperty] private string _currentDeviceKind = "";
+ [ObservableProperty] private string _currentEffectiveVersion = "";
+ [ObservableProperty] private string _currentEndpoint = "";
+ [ObservableProperty] private string _currentDriverType = "";
+ [ObservableProperty] private Brush _connectionBrush = new SolidColorBrush(Colors.Gray);
+ [ObservableProperty] private Brush _sessionStateBrush = new SolidColorBrush(Colors.Gray);
+ [ObservableProperty] private bool _canEnterMaintenance;
+ [ObservableProperty] private bool _canExitMaintenance;
+ [ObservableProperty] private bool _hasDevice;
+
+ // 缂撳瓨褰撳墠閫変腑鐨勮澶囩被鍨�
+ private string _currentDeviceKindStr;
+
+ public DeviceDebug()
+ {
+ DataContext = this;
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 寮哄埗鍒锋柊璁惧鍒楄〃锛堜緵闆嗘垚娴嬭瘯棰勭疆璁惧鍚庤皟鐢級
+ /// </summary>
+ public void ForceRefreshForTest()
+ {
+ try { LoadDeviceList(); }
+ catch (Exception ex) { _log.Error("ForceRefreshForTest Error.", ex); }
+ }
+
+ public override void InitFinish()
+ {
+ base.InitFinish();
+ try
+ {
+ // 鍒濆鍖栬皟璇曟暟鎹簱
+ _debugModel = new DeviceDebugModel();
+ string dbPath = Path.Combine(BaseFolder, "debug.db");
+ _debugModel.InitDatabase(dbPath);
+
+ // 浠� UtilContainer 鑾峰彇鍩虹璁炬柦鏈嶅姟
+ var container = new UtilContainer();
+ _deviceManager = container.GetUtil<IDeviceManager>("DeviceManager");
+ _deviceService = container.GetUtil<IDeviceService>("DeviceService");
+ _resourceManager = container.GetUtil<IResourceManager>("ResourceManager");
+
+ // 鍒涘缓浼氳瘽绠$悊鍣紙浼犲叆 ResourceManager锛�
+ _sessionManager = new DebugSessionManager(_debugModel, _resourceManager);
+
+ // 璁惧閰嶇疆鏁版嵁搴撹矾寰勶紙涓� DeviceConfig 鍏变韩锛�
+ _deviceConfigDbPath = Path.Combine(BaseFolder, "data.db");
+
+ log.Info("纭欢璋冭瘯妯″潡鍒濆鍖栧畬鎴�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("纭欢璋冭瘯妯″潡鍒濆鍖栧け璐�", ex);
+ ToastHelper.Error($"纭欢璋冭瘯鍒濆鍖栧け璐�: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 鍔犺浇璁惧鍒楄〃
+ /// </summary>
+ private void LoadDeviceList()
+ {
+ try
+ {
+ if (!File.Exists(_deviceConfigDbPath))
+ {
+ log.Warn($"璁惧閰嶇疆鏁版嵁搴撲笉瀛樺湪: {_deviceConfigDbPath}");
+ ToastHelper.Warning("璁惧閰嶇疆鏁版嵁搴撲笉瀛樺湪锛岃鍏堝湪璁惧閰嶇疆妯″潡涓坊鍔犺澶�");
+ return;
+ }
+
+ var sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
+ var dt = sqliteHelper.GetDataTable("SELECT DeviceId, Kind FROM DeviceDefinition ORDER BY DeviceId");
+
+ var list = new ObservableCollection<string>();
+ foreach (System.Data.DataRow row in dt.Rows)
+ {
+ string deviceId = row["DeviceId"].ToString();
+ string kind = row["Kind"].ToString();
+ list.Add($"{deviceId} [{kind}]");
+ }
+
+ DeviceList = list;
+ log.Info($"鍔犺浇璁惧鍒楄〃瀹屾垚锛屽叡 {list.Count} 涓澶�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍔犺浇璁惧鍒楄〃澶辫触", ex);
+ ToastHelper.Error($"鍔犺浇璁惧鍒楄〃澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 瑙f瀽璁惧閫夋嫨瀛楃涓诧紝鑾峰彇 DeviceId
+ /// </summary>
+ private string ParseDeviceId(string selection)
+ {
+ if (string.IsNullOrEmpty(selection)) return string.Empty;
+ int idx = selection.IndexOf(" [");
+ return idx > 0 ? selection.Substring(0, idx) : selection;
+ }
+
+ /// <summary>
+ /// 瑙f瀽璁惧閫夋嫨瀛楃涓诧紝鑾峰彇 Kind
+ /// </summary>
+ private string ParseDeviceKind(string selection)
+ {
+ if (string.IsNullOrEmpty(selection)) return string.Empty;
+ int start = selection.IndexOf(" [");
+ int end = selection.IndexOf("]");
+ if (start >= 0 && end > start)
+ return selection.Substring(start + 2, end - start - 2);
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// 鍔犺浇璁惧璇︾粏淇℃伅
+ /// </summary>
+ private void LoadDeviceInfo(string deviceId)
+ {
+ try
+ {
+ if (!File.Exists(_deviceConfigDbPath)) return;
+
+ var sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
+ var dt = sqliteHelper.GetDataTable(
+ $"SELECT DeviceId, Kind, EffectiveVersion, Endpoint, DriverType FROM DeviceDefinition WHERE DeviceId = '{deviceId}'");
+
+ if (dt.Rows.Count > 0)
+ {
+ var row = dt.Rows[0];
+ CurrentDeviceId = row["DeviceId"].ToString();
+ CurrentDeviceKind = row["Kind"].ToString();
+ CurrentEffectiveVersion = row["EffectiveVersion"]?.ToString() ?? "";
+ CurrentEndpoint = row["Endpoint"]?.ToString() ?? "";
+ CurrentDriverType = row["DriverType"]?.ToString() ?? "";
+
+ // 缂撳瓨璁惧绫诲瀷
+ _currentDeviceKindStr = CurrentDeviceKind;
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍔犺浇璁惧淇℃伅澶辫触: {deviceId}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 閫夋嫨璁惧鏃讹細鎵撳紑浼氳瘽锛圧eadOnly锛夛紝鍔犺浇瀵瑰簲闈㈡澘
+ /// </summary>
+ private void OnDeviceSelected()
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(SelectedDevice))
+ {
+ HasDevice = false;
+ panelContainer.Content = new TextBlock
+ {
+ Text = "璇烽�夋嫨璁惧",
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ FontSize = 16,
+ Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999"))
+ };
+ return;
+ }
+
+ string deviceId = ParseDeviceId(SelectedDevice);
+ string kind = ParseDeviceKind(SelectedDevice);
+
+ // 鍔犺浇璁惧璇︾粏淇℃伅
+ LoadDeviceInfo(deviceId);
+
+ HasDevice = true;
+
+ // 鎵撳紑浼氳瘽锛圧eadOnly锛�
+ CurrentSession = _sessionManager.OpenSession(deviceId, "", "operator", CurrentEffectiveVersion);
+
+ // 閫氳繃 DeviceManager 杩炴帴璁惧
+ if (_deviceManager != null)
+ {
+ try
+ {
+ _deviceManager.ConnectAsync(deviceId, CancellationToken.None)
+ .GetAwaiter().GetResult();
+ ConnectionStateText = "宸茶繛鎺�";
+ ConnectionBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
+ }
+ catch (Exception ex)
+ {
+ log.Warn($"閫氳繃 DeviceManager 杩炴帴璁惧澶辫触: {ex.Message}");
+ ConnectionStateText = "杩炴帴澶辫触";
+ ConnectionBrush = new SolidColorBrush(Colors.OrangeRed);
+ }
+ }
+
+ // 鍔犺浇鎿嶄綔鏃ュ織
+ LoadOperationLogs();
+
+ // 鏇存柊鎸夐挳鐘舵��
+ UpdateButtonStates();
+
+ // 鍔犺浇瀵瑰簲闈㈡澘
+ LoadPanel(kind);
+
+ log.Info($"宸查�夋嫨璁惧: {deviceId}, 绫诲瀷: {kind}");
+ }
+ catch (Exception ex)
+ {
+ log.Error("閫夋嫨璁惧澶辫触", ex);
+ ToastHelper.Error($"閫夋嫨璁惧澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 鏍规嵁璁惧绫诲瀷鍔犺浇瀵瑰簲鐨勮皟璇曢潰鏉�
+ /// </summary>
+ private void LoadPanel(string deviceKind)
+ {
+ try
+ {
+ UIElement panel = null;
+
+ switch (deviceKind)
+ {
+ case "PowerSupply":
+ var powerPanel = new PowerSupplyDebugPanel();
+ powerPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive;
+ powerPanel.OnCommandExecute += Panel_OnCommandExecute;
+ panel = powerPanel;
+ break;
+ case "CanDevice":
+ var canPanel = new CanDebugPanel
+ {
+ DeviceId = CurrentDeviceId,
+ InitialBusName = ParseCanBusName(CurrentEndpoint),
+ InitialNodeIndex = ParseCanNodeIndex(CurrentEndpoint),
+ AutoConnectOnLoad = true
+ };
+ canPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive;
+ canPanel.OnCommandExecute += Panel_OnCommandExecute;
+ panel = canPanel;
+ break;
+ case "PlcDevice":
+ var plcPanel = new PlcDebugPanel();
+ plcPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive;
+ plcPanel.OnCommandExecute += Panel_OnCommandExecute;
+ panel = plcPanel;
+ break;
+ default:
+ panel = new TextBlock
+ {
+ Text = $"涓嶆敮鎸佺殑璁惧绫诲瀷: {deviceKind}",
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ FontSize = 14,
+ Foreground = new SolidColorBrush(Colors.OrangeRed)
+ };
+ break;
+ }
+
+ panelContainer.Content = panel;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍔犺浇璋冭瘯闈㈡澘澶辫触: {deviceKind}", ex);
+ ToastHelper.Error($"鍔犺浇闈㈡澘澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 闈㈡澘鍛戒护鎵ц浜嬩欢澶勭悊
+ /// </summary>
+ private void Panel_OnCommandExecute(object sender, DebugCommand command)
+ {
+ try
+ {
+ if (CurrentSession == null)
+ {
+ ToastHelper.Warning("璇峰厛閫夋嫨璁惧");
+ return;
+ }
+
+ // 楠岃瘉鍛戒护鏉冮檺
+ if (!_sessionManager.ValidateCommand(CurrentSession.SessionId, command))
+ {
+ ToastHelper.Warning("褰撳墠浼氳瘽鐘舵�佷笉鍏佽鎵ц姝ゆ搷浣滐紝璇峰厛杩涘叆缁存姢妯″紡");
+ return;
+ }
+
+ // 閫氳繃 DeviceService 鎵ц瀹為檯鍛戒护
+ if (_deviceService != null)
+ {
+ ExecuteViaDeviceService(command);
+ }
+ else
+ {
+ // 闄嶇骇妯″紡锛氬熀纭�璁炬柦鏈垵濮嬪寲鏃朵繚鐣欏師鏈夎涓�
+ log.Warn("DeviceService 鏈垵濮嬪寲锛屼娇鐢ㄩ檷绾фā寮忚褰曞懡浠�");
+ ExecuteFallback(command);
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鎵ц鍛戒护澶辫触: {command.CommandType}", ex);
+ ToastHelper.Error($"鍛戒护鎵ц澶辫触: {ex.Message}");
+
+ // 璁板綍澶辫触鎿嶄綔
+ var operation = new DebugOperation
+ {
+ SessionId = CurrentSession?.SessionId ?? "",
+ CommandType = command.CommandType,
+ Parameters = SerializeParameters(command),
+ Outcome = "Failed",
+ ErrorMessage = ex.Message,
+ Timestamp = DateTime.Now
+ };
+ _debugModel.RecordOperation(operation);
+ OperationLogs.Insert(0, operation);
+ }
+ }
+
+ /// <summary>
+ /// 閫氳繃 DeviceService 鎵ц璁惧鍛戒护
+ /// </summary>
+ private void ExecuteViaDeviceService(DebugCommand command)
+ {
+ try
+ {
+ var ownerToken = new ResourceOwnerToken(
+ OwnerType.Maintenance,
+ CurrentSession.SessionId);
+
+ var deviceCommand = BuildDeviceCommand(command);
+
+ var result = _deviceService.ExecuteCommandAsync(
+ command.DeviceId,
+ deviceCommand,
+ ownerToken,
+ CancellationToken.None).GetAwaiter().GetResult();
+
+ var operation = new DebugOperation
+ {
+ SessionId = CurrentSession.SessionId,
+ CommandType = command.CommandType,
+ Parameters = SerializeParameters(command),
+ Outcome = result.Success ? "Success" : "Failed",
+ DeviceReadback = result.Readback ?? "",
+ ErrorCode = result.ErrorCode ?? "",
+ ErrorMessage = result.ErrorMessage ?? "",
+ Timestamp = DateTime.Now
+ };
+
+ _debugModel.RecordOperation(operation);
+ OperationLogs.Insert(0, operation);
+
+ if (result.Success)
+ ToastHelper.Success($"鍛戒护 {command.CommandType} 鎵ц鎴愬姛");
+ else
+ ToastHelper.Warning($"鍛戒护鎵ц澶辫触: {result.ErrorMessage}");
+
+ log.Info($"鎵ц鍛戒护: {command.CommandType}, 璁惧: {command.DeviceId}, 缁撴灉: {(result.Success ? "鎴愬姛" : "澶辫触")}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"DeviceService 鎵ц鍛戒护澶辫触: {ex.Message}", ex);
+ ToastHelper.Error($"璁惧閫氫俊澶辫触: {ex.Message}");
+
+ var operation = new DebugOperation
+ {
+ SessionId = CurrentSession.SessionId,
+ CommandType = command.CommandType,
+ Parameters = SerializeParameters(command),
+ Outcome = "Failed",
+ ErrorMessage = ex.Message,
+ Timestamp = DateTime.Now
+ };
+ _debugModel.RecordOperation(operation);
+ OperationLogs.Insert(0, operation);
+ }
+ }
+
+ /// <summary>
+ /// 闄嶇骇妯″紡锛氭棤 DeviceService 鏃剁殑鍚庡澶勭悊
+ /// </summary>
+ private void ExecuteFallback(DebugCommand command)
+ {
+ var operation = new DebugOperation
+ {
+ SessionId = CurrentSession.SessionId,
+ CommandType = command.CommandType,
+ Parameters = SerializeParameters(command),
+ Outcome = "Success",
+ Timestamp = DateTime.Now
+ };
+
+ _debugModel.RecordOperation(operation);
+ OperationLogs.Insert(0, operation);
+
+ ToastHelper.Success($"鍛戒护 {command.CommandType} 宸插彂閫侊紙闄嶇骇妯″紡锛�");
+ log.Info($"[闄嶇骇妯″紡] 鎵ц鍛戒护: {command.CommandType}, 璁惧: {command.DeviceId}");
+ }
+
+ /// <summary>
+ /// 鏍规嵁鍛戒护绫诲瀷鏋勫缓瀹為檯璁惧鍛戒护瀛楃涓�
+ /// </summary>
+ private string BuildDeviceCommand(DebugCommand command)
+ {
+ if (command is PowerSetCommand psc)
+ {
+ return $"VOLT {psc.Voltage};CURR {psc.CurrentLimit};OUTP {(psc.Output ? "ON" : "OFF")}";
+ }
+ else if (command is PowerMeasureCommand)
+ {
+ return "MEAS:ALL?";
+ }
+ else if (command is CanSendCommand csc)
+ {
+ return $"CAN:SEND {csc.Frame}";
+ }
+ else if (command is PlcWriteCommand pwc)
+ {
+ return $"WRITE:{pwc.IoAddress}:{(pwc.Value ? "TRUE" : "FALSE")}";
+ }
+ return command.CommandType;
+ }
+
+ /// <summary>
+ /// 搴忓垪鍖栧懡浠ゅ弬鏁颁负JSON瀛楃涓�
+ /// </summary>
+ private string SerializeParameters(DebugCommand command)
+ {
+ try
+ {
+ switch (command)
+ {
+ case PowerSetCommand psc:
+ return $"{{Voltage:{psc.Voltage},CurrentLimit:{psc.CurrentLimit},Output:{psc.Output}}}";
+ case PowerMeasureCommand _:
+ return "{}";
+ case CanSendCommand csc:
+ return $"{{Frame:{csc.Frame},Data:{BitConverter.ToString(csc.Data)}}}";
+ case PlcWriteCommand pwc:
+ return $"{{IoAddress:{pwc.IoAddress},Value:{pwc.Value}}}";
+ default:
+ return "{}";
+ }
+ }
+ catch
+ {
+ return "{}";
+ }
+ }
+
+ /// <summary>
+ /// 鍔犺浇鎿嶄綔鏃ュ織
+ /// </summary>
+ private void LoadOperationLogs()
+ {
+ try
+ {
+ if (CurrentSession == null) return;
+
+ var ops = _debugModel.GetOperationsBySession(CurrentSession.SessionId);
+ OperationLogs = new ObservableCollection<DebugOperation>(ops.Take(100));
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍔犺浇鎿嶄綔鏃ュ織澶辫触", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊鎸夐挳鍚敤鐘舵��
+ /// </summary>
+ private void UpdateButtonStates()
+ {
+ if (CurrentSession == null)
+ {
+ CanEnterMaintenance = false;
+ CanExitMaintenance = false;
+ SessionStateText = "";
+ return;
+ }
+
+ var state = CurrentSession.State;
+ CanEnterMaintenance = state == DebugSessionState.ReadOnly;
+ CanExitMaintenance = state == DebugSessionState.MaintenanceActive;
+
+ // 鏇存柊浼氳瘽鐘舵�佹枃鏈拰棰滆壊
+ SessionStateText = state.ToString();
+ SessionStateBrush = state switch
+ {
+ DebugSessionState.ReadOnly => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2196F3")),
+ DebugSessionState.MaintenanceActive => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")),
+ DebugSessionState.Faulted => new SolidColorBrush(Colors.Red),
+ DebugSessionState.RecoveryRequired => new SolidColorBrush(Colors.Orange),
+ _ => new SolidColorBrush(Colors.Gray)
+ };
+
+ // 鏇存柊闈㈡澘鐨勭淮鎶ゆā寮�
+ if (panelContainer.Content is PowerSupplyDebugPanel powerPanel)
+ powerPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive;
+ else if (panelContainer.Content is CanDebugPanel canPanel)
+ canPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive;
+ else if (panelContainer.Content is PlcDebugPanel plcPanel)
+ plcPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive;
+ }
+
+ #region 浜嬩欢澶勭悊
+
+ private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ LoadDeviceList();
+ Loaded -= SimpleMaskControl_Loaded;
+ }
+
+ private void DeviceSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ OnDeviceSelected();
+ }
+
+ private void RfhBtn_Click(object sender, RoutedEventArgs e)
+ {
+ LoadDeviceList();
+ }
+
+ private void EnterMaintenanceBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (CurrentSession == null) return;
+
+ // 璇锋眰缁存姢妯″紡
+ if (_sessionManager.RequestMaintenance(CurrentSession.SessionId))
+ {
+ // 婵�娲荤淮鎶ゆā寮�
+ _sessionManager.ActivateMaintenance(CurrentSession.SessionId);
+ UpdateButtonStates();
+
+ // 閲嶆柊鍔犺浇闈㈡澘浠ユ洿鏂癐sMaintenanceMode
+ LoadPanel(CurrentDeviceKind);
+
+ ToastHelper.Success("宸茶繘鍏ョ淮鎶ゆ帶鍒舵ā寮�");
+ log.Info($"浼氳瘽 {CurrentSession.SessionId} 杩涘叆缁存姢妯″紡");
+ }
+ else
+ {
+ ToastHelper.Warning("鏃犳硶杩涘叆缁存姢妯″紡锛岃妫�鏌ヨ澶囩姸鎬�");
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("杩涘叆缁存姢妯″紡澶辫触", ex);
+ ToastHelper.Error($"杩涘叆缁存姢妯″紡澶辫触: {ex.Message}");
+ }
+ }
+
+ private void ExitMaintenanceBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (CurrentSession == null) return;
+
+ if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), "纭畾瑕侀��鍑虹淮鎶ゆā寮忓悧锛�") == MessageBoxResult.OK)
+ {
+ _sessionManager.CloseSession(CurrentSession.SessionId);
+ CurrentSession = null;
+ UpdateButtonStates();
+
+ // 娓呯┖闈㈡澘
+ panelContainer.Content = new TextBlock
+ {
+ Text = "璇烽�夋嫨璁惧",
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ FontSize = 16,
+ Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999"))
+ };
+
+ HasDevice = false;
+ SelectedDevice = null;
+
+ ToastHelper.Success("宸查��鍑虹淮鎶ゆā寮�");
+ log.Info("宸查��鍑虹淮鎶ゆā寮�");
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error("閫�鍑虹淮鎶ゆā寮忓け璐�", ex);
+ ToastHelper.Error($"閫�鍑虹淮鎶ゆā寮忓け璐�: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 浠� CAN Endpoint 瑙f瀽鎬荤嚎鍚嶇О锛堟牸寮�: 鎬荤嚎鍚�:鑺傜偣缂栧彿锛�
+ /// </summary>
+ private string ParseCanBusName(string endpoint)
+ {
+ if (string.IsNullOrEmpty(endpoint)) return "CAN_BUS_1";
+ int idx = endpoint.IndexOf(':');
+ return idx > 0 ? endpoint.Substring(0, idx) : endpoint;
+ }
+
+ /// <summary>
+ /// 浠� CAN Endpoint 瑙f瀽鑺傜偣缂栧彿
+ /// </summary>
+ private int? ParseCanNodeIndex(string endpoint)
+ {
+ if (string.IsNullOrEmpty(endpoint)) return 0;
+ int idx = endpoint.IndexOf(':');
+ if (idx > 0 && idx < endpoint.Length - 1 && int.TryParse(endpoint.Substring(idx + 1), out int n))
+ return n;
+ return 0;
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugin.DeviceDebug/Entity/DebugCommand.cs b/Plugin.DeviceDebug/Entity/DebugCommand.cs
new file mode 100644
index 0000000..55f7802
--- /dev/null
+++ b/Plugin.DeviceDebug/Entity/DebugCommand.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceDebug.Entity
+{
+ /// <summary>
+ /// 璋冭瘯鍛戒护鍩虹被
+ /// </summary>
+ public partial class DebugCommand : ObservableObject
+ {
+ public DebugCommand()
+ {
+ CommandId = Guid.NewGuid().ToString();
+ CreatedAt = DateTime.Now;
+ Parameters = new Dictionary<string, object>();
+ }
+
+ [ObservableProperty] private string _commandId = string.Empty;
+ [ObservableProperty] private string _commandType = string.Empty;
+ [ObservableProperty] private string _deviceId = string.Empty;
+ [ObservableProperty] private int _channel;
+ [ObservableProperty] private Dictionary<string, object> _parameters = new();
+ [ObservableProperty] private DateTime _createdAt = DateTime.Now;
+ }
+
+ /// <summary>
+ /// 鐢垫簮璁剧疆鍛戒护
+ /// </summary>
+ public class PowerSetCommand : DebugCommand
+ {
+ public PowerSetCommand() { CommandType = "PowerSet"; }
+ public double Voltage { get; set; }
+ public double CurrentLimit { get; set; }
+ public bool Output { get; set; }
+ }
+
+ /// <summary>
+ /// 鐢垫簮娴嬮噺鍛戒护
+ /// </summary>
+ public class PowerMeasureCommand : DebugCommand
+ {
+ public PowerMeasureCommand() { CommandType = "PowerMeasure"; }
+ }
+
+ /// <summary>
+ /// CAN鍙戦�佸懡浠�
+ /// </summary>
+ public class CanSendCommand : DebugCommand
+ {
+ public CanSendCommand() { CommandType = "CanSend"; }
+ public string Frame { get; set; } = string.Empty;
+ public byte[] Data { get; set; } = Array.Empty<byte>();
+ }
+
+ /// <summary>
+ /// PLC鍐欏叆鍛戒护
+ /// </summary>
+ public class PlcWriteCommand : DebugCommand
+ {
+ public PlcWriteCommand() { CommandType = "PlcWrite"; }
+ public string IoAddress { get; set; } = string.Empty;
+ public bool Value { get; set; }
+ }
+}
diff --git a/Plugin.DeviceDebug/Entity/DebugOperation.cs b/Plugin.DeviceDebug/Entity/DebugOperation.cs
new file mode 100644
index 0000000..1a1d165
--- /dev/null
+++ b/Plugin.DeviceDebug/Entity/DebugOperation.cs
@@ -0,0 +1,27 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceDebug.Entity
+{
+ /// <summary>
+ /// 鎿嶄綔瀹¤瀹炰綋 - 璁板綍姣忔璋冭瘯鎿嶄綔
+ /// </summary>
+ public partial class DebugOperation : ObservableObject
+ {
+ public DebugOperation()
+ {
+ OperationId = Guid.NewGuid().ToString();
+ Timestamp = DateTime.Now;
+ }
+
+ [ObservableProperty] private string _operationId = string.Empty;
+ [ObservableProperty] private string _sessionId = string.Empty; // FK 鈫� DebugSession
+ [ObservableProperty] private string _commandType = string.Empty; // PowerSet/PowerGet/OutputOn/OutputOff/CanSend/PlcWrite绛�
+ [ObservableProperty] private string _parameters = string.Empty; // JSON瀛楃涓�
+ [ObservableProperty] private string _outcome = string.Empty; // Success/Failed/Rejected/Timeout
+ [ObservableProperty] private string _deviceReadback = string.Empty;
+ [ObservableProperty] private string _errorCode = string.Empty;
+ [ObservableProperty] private string _errorMessage = string.Empty;
+ [ObservableProperty] private DateTime _timestamp = DateTime.Now;
+ }
+}
diff --git a/Plugin.DeviceDebug/Entity/DebugSession.cs b/Plugin.DeviceDebug/Entity/DebugSession.cs
new file mode 100644
index 0000000..2f2d582
--- /dev/null
+++ b/Plugin.DeviceDebug/Entity/DebugSession.cs
@@ -0,0 +1,75 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceDebug.Entity
+{
+ /// <summary>
+ /// 璋冭瘯浼氳瘽鐘舵�佹灇涓�
+ /// </summary>
+ public enum DebugSessionState
+ {
+ /// <summary>鍙妯″紡锛堥粯璁わ級</summary>
+ ReadOnly = 0,
+ /// <summary>姝e湪鐢宠缁存姢鏉冮檺</summary>
+ AcquiringMaintenance = 1,
+ /// <summary>缁存姢妯″紡婵�娲�</summary>
+ MaintenanceActive = 2,
+ /// <summary>姝e湪鍏抽棴</summary>
+ Closing = 3,
+ /// <summary>宸插叧闂�</summary>
+ Closed = 4,
+ /// <summary>鏁呴殰</summary>
+ Faulted = 5,
+ /// <summary>闇�瑕佹仮澶�</summary>
+ RecoveryRequired = 6
+ }
+
+ /// <summary>
+ /// 璋冭瘯浼氳瘽瀹炰綋
+ /// </summary>
+ public partial class DebugSession : ObservableObject
+ {
+ public DebugSession()
+ {
+ SessionId = Guid.NewGuid().ToString();
+ CreatedAt = DateTime.Now;
+ State = DebugSessionState.ReadOnly;
+ }
+
+ /// <summary>浼氳瘽ID锛堜富閿級</summary>
+ [ObservableProperty]
+ private string _sessionId = string.Empty;
+
+ /// <summary>璁惧ID</summary>
+ [ObservableProperty]
+ private string _deviceId = string.Empty;
+
+ /// <summary>鎿嶄綔鍛業D</summary>
+ [ObservableProperty]
+ private string _operatorId = string.Empty;
+
+ /// <summary>鎿嶄綔鍛樺悕绉�</summary>
+ [ObservableProperty]
+ private string _operatorName = string.Empty;
+
+ /// <summary>鐢熸晥鐗堟湰</summary>
+ [ObservableProperty]
+ private string _effectiveVersion = string.Empty;
+
+ /// <summary>浼氳瘽鐘舵��</summary>
+ [ObservableProperty]
+ private DebugSessionState _state = DebugSessionState.ReadOnly;
+
+ /// <summary>棰勭害寮曠敤</summary>
+ [ObservableProperty]
+ private string _reservationRef = string.Empty;
+
+ /// <summary>鍒涘缓鏃堕棿</summary>
+ [ObservableProperty]
+ private DateTime _createdAt = DateTime.Now;
+
+ /// <summary>鍏抽棴鏃堕棿</summary>
+ [ObservableProperty]
+ private DateTime? _closedAt;
+ }
+}
diff --git a/Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs b/Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs
new file mode 100644
index 0000000..269a6a4
--- /dev/null
+++ b/Plugin.DeviceDebug/Entity/DeviceRuntimeStatus.cs
@@ -0,0 +1,44 @@
+using System;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Plugin.DeviceDebug.Entity
+{
+ public enum DeviceConnectionState
+ {
+ Connected,
+ Disconnected,
+ Connecting,
+ Error
+ }
+
+ public enum DeviceHealth
+ {
+ Healthy,
+ Degraded,
+ Fault
+ }
+
+ public enum ReservationStatus
+ {
+ Available,
+ ProductionReserved,
+ MaintenanceReserved,
+ RecoveryRequired
+ }
+
+ /// <summary>
+ /// 璁惧杩愯鐘舵�侊紙鍐呭瓨瀵硅薄锛屼笉鎸佷箙鍖栵級
+ /// </summary>
+ public partial class DeviceRuntimeStatus : ObservableObject
+ {
+ [ObservableProperty] private string _deviceId = string.Empty;
+ [ObservableProperty] private DeviceConnectionState _connectionState = DeviceConnectionState.Disconnected;
+ [ObservableProperty] private DeviceHealth _health = DeviceHealth.Healthy;
+ [ObservableProperty] private string _lastError = string.Empty;
+ [ObservableProperty] private DateTime? _lastErrorTime;
+ [ObservableProperty] private ReservationStatus _reservationStatus = ReservationStatus.Available;
+ [ObservableProperty] private string _effectiveVersion = string.Empty;
+ [ObservableProperty] private int _reconnectCount;
+ [ObservableProperty] private double _communicationLatencyMs;
+ }
+}
diff --git a/Plugin.DeviceDebug/Model/DebugSessionManager.cs b/Plugin.DeviceDebug/Model/DebugSessionManager.cs
new file mode 100644
index 0000000..334c0b1
--- /dev/null
+++ b/Plugin.DeviceDebug/Model/DebugSessionManager.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using log4net;
+using Plugin.DeviceDebug.Entity;
+using Aging.Infrastructure.Contracts;
+
+namespace Plugin.DeviceDebug.Model
+{
+ /// <summary>
+ /// 璋冭瘯浼氳瘽绠$悊鍣� - 璐熻矗鐘舵�佹満杞崲鍜屾潈闄愭牎楠�
+ /// </summary>
+ public class DebugSessionManager
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(DebugSessionManager));
+ private readonly DeviceDebugModel _model;
+ private readonly IResourceManager? _resourceManager;
+
+ // 鍐呭瓨涓殑娲昏穬浼氳瘽缂撳瓨
+ private readonly Dictionary<string, DebugSession> _activeSessions = new();
+
+ public DebugSessionManager(DeviceDebugModel model, IResourceManager? resourceManager = null)
+ {
+ _model = model;
+ _resourceManager = resourceManager;
+ }
+
+ /// <summary>
+ /// 鎵撳紑浼氳瘽锛圧eadOnly妯″紡锛�
+ /// </summary>
+ public DebugSession OpenSession(string deviceId, string operatorId, string operatorName, string effectiveVersion)
+ {
+ try
+ {
+ // 妫�鏌ユ槸鍚﹀凡鏈夋椿璺冧細璇�
+ var existingSession = GetActiveSessionByDevice(deviceId);
+ if (existingSession != null)
+ {
+ log.Warn($"璁惧 {deviceId} 宸叉湁娲昏穬浼氳瘽: {existingSession.SessionId}");
+ return existingSession;
+ }
+
+ var session = new DebugSession
+ {
+ DeviceId = deviceId,
+ OperatorId = operatorId,
+ OperatorName = operatorName,
+ EffectiveVersion = effectiveVersion,
+ State = DebugSessionState.ReadOnly
+ };
+
+ // 鎸佷箙鍖栧埌鏁版嵁搴�
+ _model.CreateSession(session);
+
+ // 鏇存柊鍐呭瓨缂撳瓨
+ _activeSessions[session.SessionId] = session;
+
+ log.Info($"鎵撳紑浼氳瘽: {session.SessionId}, 璁惧: {deviceId}, 鎿嶄綔鍛�: {operatorName}");
+ return session;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鎵撳紑浼氳瘽澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 璇锋眰杩涘叆缁存姢妯″紡锛圧eadOnly 鈫� AcquiringMaintenance锛�
+ /// 妫�鏌ワ細璁惧鏄惁鏈夌敓浜eserved
+ /// </summary>
+ public bool RequestMaintenance(string sessionId)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ return false;
+ }
+
+ // 楠岃瘉鐘舵�佽浆鎹�
+ if (session.State != DebugSessionState.ReadOnly)
+ {
+ log.Warn($"浼氳瘽 {sessionId} 褰撳墠鐘舵�佷负 {session.State}锛屾棤娉曡姹傜淮鎶ゆā寮�");
+ return false;
+ }
+
+ // 妫�鏌ヨ澶囨槸鍚︽湁鐢熶骇Reserved锛堜笌璧勬簮棰勭害绯荤粺闆嗘垚锛�
+ if (_resourceManager != null)
+ {
+ var resourceKey = new ResourceKey(session.DeviceId, "Generic");
+ var ownerToken = new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId);
+
+ try
+ {
+ var reservation = _resourceManager.ReserveAsync(
+ ownerToken, resourceKey, CancellationToken.None).GetAwaiter().GetResult();
+ session.ReservationRef = reservation.ReservationId;
+ }
+ catch (ResourceConflictException)
+ {
+ log.Warn($"璁惧 {session.DeviceId} 宸茶鐢熶骇棰勭害鍗犵敤锛屾嫆缁濈淮鎶よ姹�");
+ return false;
+ }
+ }
+ else
+ {
+ // 闄嶇骇妯″紡锛氭棤 ResourceManager 鏃跺厑璁告墍鏈夎姹傦紙鍚戝悗鍏煎锛�
+ log.Warn("ResourceManager 鏈垵濮嬪寲锛岃烦杩囪祫婧愰绾︽鏌�");
+ }
+
+ // 鐘舵�佽浆鎹�
+ session.State = DebugSessionState.AcquiringMaintenance;
+
+ // 鎸佷箙鍖�
+ _model.UpdateSession(session);
+
+ log.Info($"浼氳瘽 {sessionId} 璇锋眰缁存姢妯″紡");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"璇锋眰缁存姢妯″紡澶辫触: {ex.Message}", ex);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// 婵�娲荤淮鎶ゆā寮忥紙AcquiringMaintenance 鈫� MaintenanceActive锛�
+ /// </summary>
+ public void ActivateMaintenance(string sessionId)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ throw new InvalidOperationException($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ }
+
+ // 楠岃瘉鐘舵�佽浆鎹�
+ if (session.State != DebugSessionState.AcquiringMaintenance)
+ {
+ log.Warn($"浼氳瘽 {sessionId} 褰撳墠鐘舵�佷负 {session.State}锛屾棤娉曟縺娲荤淮鎶ゆā寮�");
+ throw new InvalidOperationException($"浼氳瘽 {sessionId} 褰撳墠鐘舵�佷负 {session.State}锛屾棤娉曟縺娲荤淮鎶ゆā寮�");
+ }
+
+ // 鐘舵�佽浆鎹�
+ session.State = DebugSessionState.MaintenanceActive;
+
+ // 鎸佷箙鍖�
+ _model.UpdateSession(session);
+
+ log.Info($"浼氳瘽 {sessionId} 婵�娲荤淮鎶ゆā寮�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"婵�娲荤淮鎶ゆā寮忓け璐�: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 鍏抽棴浼氳瘽锛圡aintenanceActive 鈫� Closing 鈫� Closed锛�
+ /// 鎵ц瀹夊叏鐘舵�佹鏌�
+ /// </summary>
+ public void CloseSession(string sessionId)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ throw new InvalidOperationException($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ }
+
+ // 楠岃瘉鐘舵�佽浆鎹紙鍙湁 MaintenanceActive 鍙互鍏抽棴锛�
+ if (session.State != DebugSessionState.MaintenanceActive)
+ {
+ log.Warn($"浼氳瘽 {sessionId} 褰撳墠鐘舵�佷负 {session.State}锛屾棤娉曞叧闂�");
+ throw new InvalidOperationException($"浼氳瘽 {sessionId} 褰撳墠鐘舵�佷负 {session.State}锛屾棤娉曞叧闂�");
+ }
+
+ // 鐘舵�佽浆鎹�: MaintenanceActive 鈫� Closing
+ session.State = DebugSessionState.Closing;
+ _model.UpdateSession(session);
+ log.Debug($"浼氳瘽 {sessionId} 杩涘叆鍏抽棴涓姸鎬�");
+
+ // 鎵ц瀹夊叏鐘舵�佹鏌ワ細閲婃斁璧勬簮棰勭害
+ if (_resourceManager != null && !string.IsNullOrEmpty(session.ReservationRef))
+ {
+ try
+ {
+ var resourceKey = new ResourceKey(session.DeviceId, "Generic");
+ var ownerToken = new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId);
+
+ _resourceManager.ReleaseReservationAsync(
+ ownerToken, resourceKey, CancellationToken.None).GetAwaiter().GetResult();
+
+ log.Info($"宸查噴鏀捐澶� {session.DeviceId} 鐨勮祫婧愰绾�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"閲婃斁璧勬簮棰勭害澶辫触: {ex.Message}", ex);
+ // 涓嶉樆姝㈠叧闂祦绋嬶紝浣嗚褰曡鍛�
+ }
+ }
+
+ // 鐘舵�佽浆鎹�: Closing 鈫� Closed
+ session.State = DebugSessionState.Closed;
+ session.ClosedAt = DateTime.Now;
+ _model.UpdateSession(session);
+
+ // 浠庣紦瀛樹腑绉婚櫎
+ _activeSessions.Remove(sessionId);
+
+ log.Info($"浼氳瘽 {sessionId} 宸插叧闂�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍏抽棴浼氳瘽澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 鏁呴殰澶勭悊锛堜换鎰� 鈫� Faulted锛�
+ /// </summary>
+ public void FaultSession(string sessionId, string errorMessage)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ return;
+ }
+
+ // 璁板綍褰撳墠鐘舵��
+ var previousState = session.State;
+
+ // 鐘舵�佽浆鎹�
+ session.State = DebugSessionState.Faulted;
+
+ // 鎸佷箙鍖�
+ _model.UpdateSession(session);
+
+ log.Error($"浼氳瘽 {sessionId} 鍙戠敓鏁呴殰锛屼粠 {previousState} 杞负 Faulted: {errorMessage}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鏍囪鏁呴殰鐘舵�佸け璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏍囪闇�瑕佹仮澶嶏紙浠绘剰 鈫� RecoveryRequired锛�
+ /// </summary>
+ public void MarkRecoveryRequired(string sessionId)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ return;
+ }
+
+ // 璁板綍褰撳墠鐘舵��
+ var previousState = session.State;
+
+ // 鐘舵�佽浆鎹�
+ session.State = DebugSessionState.RecoveryRequired;
+
+ // 鎸佷箙鍖�
+ _model.UpdateSession(session);
+
+ log.Warn($"浼氳瘽 {sessionId} 鏍囪涓洪渶瑕佹仮澶嶏紝浠� {previousState} 杞负 RecoveryRequired");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鏍囪鎭㈠鐘舵�佸け璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 楠岃瘉鍛戒护鏄惁鍏佽鎵ц锛堝彧鏈� MaintenanceActive 鐘舵�佸厑璁稿啓鎿嶄綔锛�
+ /// </summary>
+ public bool ValidateCommand(string sessionId, DebugCommand command)
+ {
+ try
+ {
+ if (!_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ log.Warn($"浼氳瘽涓嶅瓨鍦�: {sessionId}");
+ return false;
+ }
+
+ // 鍙湁 MaintenanceActive 鐘舵�佸厑璁稿啓鎿嶄綔
+ bool allowed = session.State == DebugSessionState.MaintenanceActive;
+
+ if (!allowed)
+ {
+ log.Warn($"浼氳瘽 {sessionId} 鐘舵�佷负 {session.State}锛屾嫆缁濇墽琛屽懡浠�: {command.CommandType}");
+ }
+
+ // 澧炲姞璧勬簮棰勭害楠岃瘉
+ if (allowed && _resourceManager != null)
+ {
+ var resourceKey = new ResourceKey(session.DeviceId, "Generic");
+ allowed = _resourceManager.ValidateOwner(
+ new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId),
+ resourceKey);
+ if (!allowed)
+ {
+ log.Warn($"浼氳瘽 {sessionId} 璧勬簮棰勭害楠岃瘉澶辫触锛屾嫆缁濇墽琛屽懡浠�: {command.CommandType}");
+ }
+ }
+
+ return allowed;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"楠岃瘉鍛戒护澶辫触: {ex.Message}", ex);
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇浼氳瘽鐘舵��
+ /// </summary>
+ public DebugSessionState? GetSessionState(string sessionId)
+ {
+ try
+ {
+ if (_activeSessions.TryGetValue(sessionId, out var session))
+ {
+ return session.State;
+ }
+
+ // 浠庢暟鎹簱鏌ヨ
+ var dbSession = _model.GetSession(sessionId);
+ return dbSession?.State;
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇浼氳瘽鐘舵�佸け璐�: {ex.Message}", ex);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇璁惧褰撳墠浼氳瘽锛堝鏋滄湁锛�
+ /// </summary>
+ public DebugSession? GetActiveSessionByDevice(string deviceId)
+ {
+ try
+ {
+ // 鍏堜粠鍐呭瓨缂撳瓨鏌ユ壘
+ var cachedSession = _activeSessions.Values
+ .FirstOrDefault(s => s.DeviceId == deviceId && s.State != DebugSessionState.Closed);
+
+ if (cachedSession != null)
+ {
+ return cachedSession;
+ }
+
+ // 浠庢暟鎹簱鏌ヨ
+ var activeSessions = _model.GetActiveSessionsByDevice(deviceId);
+ return activeSessions.FirstOrDefault();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇璁惧娲昏穬浼氳瘽澶辫触: {ex.Message}", ex);
+ return null;
+ }
+ }
+ }
+}
diff --git a/Plugin.DeviceDebug/Model/DeviceDebugModel.cs b/Plugin.DeviceDebug/Model/DeviceDebugModel.cs
new file mode 100644
index 0000000..12757cc
--- /dev/null
+++ b/Plugin.DeviceDebug/Model/DeviceDebugModel.cs
@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using HMI.Core.Util;
+using log4net;
+using Plugin.DeviceDebug.Entity;
+
+namespace Plugin.DeviceDebug.Model
+{
+ /// <summary>
+ /// 鏁版嵁璁块棶灞� - 绠$悊璋冭瘯浼氳瘽鍜屾搷浣滃璁$殑鎸佷箙鍖�
+ /// </summary>
+ public class DeviceDebugModel
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(DeviceDebugModel));
+ private SqliteHelper? _sqliteHelper;
+ private string _dbPath = string.Empty;
+
+ /// <summary>
+ /// 鍒濆鍖栨暟鎹簱锛屽垱寤哄繀瑕佺殑琛ㄧ粨鏋�
+ /// </summary>
+ public void InitDatabase(string dbPath)
+ {
+ try
+ {
+ _dbPath = dbPath;
+
+ // 纭繚鐩綍瀛樺湪
+ string? dir = Path.GetDirectoryName(dbPath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ // 濡傛灉鏁版嵁搴撴枃浠朵笉瀛樺湪锛屽垱寤哄畠
+ if (!File.Exists(dbPath))
+ {
+ SqliteHelper.CreateDatabase(dbPath);
+ log.Info($"鍒涘缓璋冭瘯鏁版嵁搴�: {dbPath}");
+ }
+
+ _sqliteHelper = new SqliteHelper(dbPath);
+
+ // 鍒涘缓 debug_session 琛�
+ string createSessionTable = @"
+ CREATE TABLE IF NOT EXISTS debug_session (
+ session_id TEXT PRIMARY KEY,
+ device_id TEXT NOT NULL,
+ operator_id TEXT NOT NULL,
+ operator_name TEXT,
+ effective_version TEXT,
+ state INTEGER NOT NULL DEFAULT 0,
+ reservation_ref TEXT,
+ created_at TEXT NOT NULL,
+ closed_at TEXT
+ )";
+
+ // 鍒涘缓 debug_operation 琛�
+ string createOperationTable = @"
+ CREATE TABLE IF NOT EXISTS debug_operation (
+ operation_id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ command_type TEXT NOT NULL,
+ parameters TEXT,
+ outcome TEXT NOT NULL,
+ device_readback TEXT,
+ error_code TEXT,
+ error_message TEXT,
+ timestamp TEXT NOT NULL,
+ FOREIGN KEY (session_id) REFERENCES debug_session(session_id)
+ )";
+
+ _sqliteHelper.ExecuteSql(createSessionTable);
+ _sqliteHelper.ExecuteSql(createOperationTable);
+
+ log.Info("璋冭瘯鏁版嵁搴撹〃鍒濆鍖栧畬鎴�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍒濆鍖栬皟璇曟暟鎹簱澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ #region 浼氳瘽 CRUD
+
+ /// <summary>
+ /// 鍒涘缓浼氳瘽
+ /// </summary>
+ public void CreateSession(DebugSession session)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ INSERT INTO debug_session (
+ session_id, device_id, operator_id, operator_name,
+ effective_version, state, reservation_ref, created_at, closed_at
+ ) VALUES (
+ '{EscapeSql(session.SessionId)}',
+ '{EscapeSql(session.DeviceId)}',
+ '{EscapeSql(session.OperatorId)}',
+ '{EscapeSql(session.OperatorName)}',
+ '{EscapeSql(session.EffectiveVersion)}',
+ {(int)session.State},
+ '{EscapeSql(session.ReservationRef)}',
+ '{session.CreatedAt:yyyy-MM-dd HH:mm:ss}',
+ {(session.ClosedAt.HasValue ? $"'{session.ClosedAt.Value:yyyy-MM-dd HH:mm:ss}'" : "NULL")}
+ )";
+
+ _sqliteHelper.ExecuteSql(sql);
+ log.Debug($"鍒涘缓浼氳瘽: {session.SessionId}, 璁惧: {session.DeviceId}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍒涘缓浼氳瘽澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇浼氳瘽
+ /// </summary>
+ public DebugSession? GetSession(string sessionId)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $"SELECT * FROM debug_session WHERE session_id = '{EscapeSql(sessionId)}'";
+ DataTable dt = _sqliteHelper.GetDataTable(sql);
+
+ if (dt.Rows.Count == 0)
+ return null;
+
+ return MapToDebugSession(dt.Rows[0]);
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇浼氳瘽澶辫触: {ex.Message}", ex);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊浼氳瘽
+ /// </summary>
+ public void UpdateSession(DebugSession session)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ UPDATE debug_session SET
+ device_id = '{EscapeSql(session.DeviceId)}',
+ operator_id = '{EscapeSql(session.OperatorId)}',
+ operator_name = '{EscapeSql(session.OperatorName)}',
+ effective_version = '{EscapeSql(session.EffectiveVersion)}',
+ state = {(int)session.State},
+ reservation_ref = '{EscapeSql(session.ReservationRef)}',
+ closed_at = {(session.ClosedAt.HasValue ? $"'{session.ClosedAt.Value:yyyy-MM-dd HH:mm:ss}'" : "NULL")}
+ WHERE session_id = '{EscapeSql(session.SessionId)}'";
+
+ _sqliteHelper.ExecuteSql(sql);
+ log.Debug($"鏇存柊浼氳瘽: {session.SessionId}, 鐘舵��: {session.State}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鏇存柊浼氳瘽澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇璁惧鐨勬墍鏈夋椿璺冧細璇濓紙鏈叧闂殑锛�
+ /// </summary>
+ public List<DebugSession> GetActiveSessionsByDevice(string deviceId)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ SELECT * FROM debug_session
+ WHERE device_id = '{EscapeSql(deviceId)}'
+ AND state != {(int)DebugSessionState.Closed}
+ ORDER BY created_at DESC";
+
+ DataTable dt = _sqliteHelper.GetDataTable(sql);
+ return dt.Rows.Cast<DataRow>().Select(MapToDebugSession).ToList();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇璁惧娲昏穬浼氳瘽澶辫触: {ex.Message}", ex);
+ return new List<DebugSession>();
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇鎵�鏈変細璇�
+ /// </summary>
+ public List<DebugSession> GetAllSessions()
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = "SELECT * FROM debug_session ORDER BY created_at DESC";
+ DataTable dt = _sqliteHelper.GetDataTable(sql);
+ return dt.Rows.Cast<DataRow>().Select(MapToDebugSession).ToList();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇鎵�鏈変細璇濆け璐�: {ex.Message}", ex);
+ return new List<DebugSession>();
+ }
+ }
+
+ #endregion
+
+ #region 瀹¤ CRUD
+
+ /// <summary>
+ /// 璁板綍鎿嶄綔
+ /// </summary>
+ public void RecordOperation(DebugOperation operation)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ INSERT INTO debug_operation (
+ operation_id, session_id, command_type, parameters,
+ outcome, device_readback, error_code, error_message, timestamp
+ ) VALUES (
+ '{EscapeSql(operation.OperationId)}',
+ '{EscapeSql(operation.SessionId)}',
+ '{EscapeSql(operation.CommandType)}',
+ '{EscapeSql(operation.Parameters)}',
+ '{EscapeSql(operation.Outcome)}',
+ '{EscapeSql(operation.DeviceReadback)}',
+ '{EscapeSql(operation.ErrorCode)}',
+ '{EscapeSql(operation.ErrorMessage)}',
+ '{operation.Timestamp:yyyy-MM-dd HH:mm:ss}'
+ )";
+
+ _sqliteHelper.ExecuteSql(sql);
+ log.Debug($"璁板綍鎿嶄綔: {operation.OperationId}, 绫诲瀷: {operation.CommandType}, 缁撴灉: {operation.Outcome}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"璁板綍鎿嶄綔澶辫触: {ex.Message}", ex);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇浼氳瘽鐨勬墍鏈夋搷浣�
+ /// </summary>
+ public List<DebugOperation> GetOperationsBySession(string sessionId)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ SELECT * FROM debug_operation
+ WHERE session_id = '{EscapeSql(sessionId)}'
+ ORDER BY timestamp DESC";
+
+ DataTable dt = _sqliteHelper.GetDataTable(sql);
+ return dt.Rows.Cast<DataRow>().Select(MapToDebugOperation).ToList();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇浼氳瘽鎿嶄綔澶辫触: {ex.Message}", ex);
+ return new List<DebugOperation>();
+ }
+ }
+
+ /// <summary>
+ /// 鑾峰彇璁惧鏈�杩戠殑鎿嶄綔
+ /// </summary>
+ public List<DebugOperation> GetRecentOperations(string deviceId, int count)
+ {
+ try
+ {
+ if (_sqliteHelper == null)
+ throw new InvalidOperationException("鏁版嵁搴撴湭鍒濆鍖栵紝璇峰厛璋冪敤 InitDatabase");
+
+ string sql = $@"
+ SELECT o.* FROM debug_operation o
+ INNER JOIN debug_session s ON o.session_id = s.session_id
+ WHERE s.device_id = '{EscapeSql(deviceId)}'
+ ORDER BY o.timestamp DESC
+ LIMIT {count}";
+
+ DataTable dt = _sqliteHelper.GetDataTable(sql);
+ return dt.Rows.Cast<DataRow>().Select(MapToDebugOperation).ToList();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鑾峰彇鏈�杩戞搷浣滃け璐�: {ex.Message}", ex);
+ return new List<DebugOperation>();
+ }
+ }
+
+ #endregion
+
+ #region 杈呭姪鏂规硶
+
+ /// <summary>
+ /// 灏� DataRow 鏄犲皠涓� DebugSession
+ /// </summary>
+ private DebugSession MapToDebugSession(DataRow row)
+ {
+ return new DebugSession
+ {
+ SessionId = row["session_id"].ToString() ?? string.Empty,
+ DeviceId = row["device_id"].ToString() ?? string.Empty,
+ OperatorId = row["operator_id"].ToString() ?? string.Empty,
+ OperatorName = row["operator_name"]?.ToString() ?? string.Empty,
+ EffectiveVersion = row["effective_version"]?.ToString() ?? string.Empty,
+ State = (DebugSessionState)Convert.ToInt32(row["state"]),
+ ReservationRef = row["reservation_ref"]?.ToString() ?? string.Empty,
+ CreatedAt = DateTime.Parse(row["created_at"].ToString() ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")),
+ ClosedAt = row["closed_at"] != DBNull.Value && !string.IsNullOrEmpty(row["closed_at"].ToString())
+ ? DateTime.Parse(row["closed_at"].ToString()!)
+ : null
+ };
+ }
+
+ /// <summary>
+ /// 灏� DataRow 鏄犲皠涓� DebugOperation
+ /// </summary>
+ private DebugOperation MapToDebugOperation(DataRow row)
+ {
+ return new DebugOperation
+ {
+ OperationId = row["operation_id"].ToString() ?? string.Empty,
+ SessionId = row["session_id"].ToString() ?? string.Empty,
+ CommandType = row["command_type"].ToString() ?? string.Empty,
+ Parameters = row["parameters"]?.ToString() ?? string.Empty,
+ Outcome = row["outcome"].ToString() ?? string.Empty,
+ DeviceReadback = row["device_readback"]?.ToString() ?? string.Empty,
+ ErrorCode = row["error_code"]?.ToString() ?? string.Empty,
+ ErrorMessage = row["error_message"]?.ToString() ?? string.Empty,
+ Timestamp = DateTime.Parse(row["timestamp"].ToString() ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))
+ };
+ }
+
+ /// <summary>
+ /// SQL 瀛楃涓茶浆涔夛紙闃叉娉ㄥ叆锛�
+ /// </summary>
+ private string EscapeSql(string value)
+ {
+ return value.Replace("'", "''");
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml b/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml
new file mode 100644
index 0000000..4ebbe7a
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml
@@ -0,0 +1,220 @@
+<UserControl x:Class="Plugin.DeviceDebug.Panels.CanDebugPanel"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ mc:Ignorable="d"
+ d:DesignHeight="700" d:DesignWidth="700">
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid dp:GridHelper.Space="10" Margin="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 鏍囬 -->
+ <Border Grid.Row="0" Background="#FF9800" CornerRadius="4" Padding="12,8">
+ <TextBlock Text="CAN 閫氳璋冭瘯闈㈡澘" FontSize="16" FontWeight="Bold" Foreground="White"/>
+ </Border>
+
+ <!-- 妯℃嫙鍣ㄨ繛鎺ユ帶鍒跺尯 -->
+ <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12" Background="#FAFAFA">
+ <Grid dp:GridHelper.Space="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="150"/>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 绗竴琛岋細鎬荤嚎鍚嶇О鍜岃妭鐐圭紪鍙� -->
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="鎬荤嚎鍚嶇О:" VerticalAlignment="Center" FontWeight="SemiBold"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="busNameInput"
+ VerticalContentAlignment="Center" Padding="6,4" Text="CAN_BUS_1"/>
+
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="鑺傜偣缂栧彿:" VerticalAlignment="Center" FontWeight="SemiBold" Margin="8,0,0,0"/>
+ <TextBox Grid.Row="0" Grid.Column="3" x:Name="nodeIndexInput"
+ VerticalContentAlignment="Center" Padding="6,4" Text="0"/>
+
+ <Button Grid.Row="0" Grid.Column="5" x:Name="connectBtn" Content="杩炴帴" Width="80" Height="30"
+ Click="ConnectBtn_Click"
+ Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+
+ <!-- 绗簩琛岋細鐘舵�佹樉绀哄拰鎺у埗鎸夐挳 -->
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="鐘舵��:" VerticalAlignment="Center" FontWeight="SemiBold"/>
+ <StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Center">
+ <Ellipse x:Name="statusIndicator" Width="12" Height="12" Fill="#9E9E9E" Margin="0,0,6,0"/>
+ <TextBlock x:Name="statusText" Text="鏈繛鎺�" VerticalAlignment="Center"/>
+ </StackPanel>
+
+ <TextBlock Grid.Row="1" Grid.Column="3" Text="缁熻:" VerticalAlignment="Center" FontWeight="SemiBold" Margin="8,0,0,0"/>
+ <TextBlock Grid.Row="1" Grid.Column="4" x:Name="statsText" Text="TX: 0 RX: 0"
+ VerticalAlignment="Center" Foreground="#666"/>
+
+ <StackPanel Grid.Row="1" Grid.Column="5" Orientation="Horizontal">
+ <ToggleButton x:Name="autoResponseToggleBtn" Content="鑷姩鍝嶅簲: 鍏�" Width="100" Height="28" Margin="0,0,4,0"
+ Click="AutoResponseToggleBtn_Click" IsEnabled="False" IsChecked="False"
+ Background="#9E9E9E" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+ <ToggleButton x:Name="maintenanceToggleBtn" Content="缁存姢妯″紡: 寮�" Width="100" Height="28" Margin="0,0,4,0"
+ Click="MaintenanceToggleBtn_Click" IsEnabled="False" IsChecked="True"
+ Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+ <Button x:Name="disconnectBtn" Content="鏂紑" Width="60" Height="28" Margin="0,0,4,0"
+ Click="DisconnectBtn_Click" IsEnabled="False"
+ Background="#F44336" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ <Button x:Name="promiscuousBtn" Content="娣锋潅妯″紡" Width="80" Height="28"
+ Click="PromiscuousBtn_Click" IsEnabled="False"
+ Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+
+ <!-- 鍙戦�佸尯 -->
+ <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12">
+ <Grid dp:GridHelper.Space="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 甯D -->
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="甯� ID:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="frameIdInput"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4" Text="0x100"/>
+
+ <!-- 鏁版嵁 -->
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="鏁版嵁(Hex):" VerticalAlignment="Center" FontWeight="Bold"/>
+ <TextBox Grid.Row="1" Grid.Column="1" x:Name="dataInput"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4" Text="00 00 00 00 00 00 00 00"/>
+
+ <!-- 鍙戦�佹寜閽� -->
+ <Button Grid.Row="1" Grid.Column="2" x:Name="sendBtn" Content="鍙戦��" Width="80" Height="30"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ Click="SendBtn_Click"
+ Background="#FF9800" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+ </Grid>
+ </Border>
+
+ <!-- 鎺ユ敹鍖� -->
+ <Border Grid.Row="3" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <DockPanel Grid.Row="0" Margin="8,6">
+ <Button x:Name="clearBtn" Content="娓呯┖" Padding="12,4" DockPanel.Dock="Right"
+ Click="ClearBtn_Click"
+ Background="#9E9E9E" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ <TextBlock Text="鎺ユ敹璁板綍" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
+ </DockPanel>
+
+ <ListView Grid.Row="1" x:Name="receiveList"
+ Style="{StaticResource DefaultListTable}"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="90" Header="鏃堕棿">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Timestamp, StringFormat=HH:mm:ss.fff}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="甯D">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding FrameId}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="50" Header="DLC">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Dlc}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="200" Header="鏁版嵁">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Data}" FontFamily="Consolas"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="50" Header="鏂瑰悜">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Text="{Binding Direction}">
+ <TextBlock.Style>
+ <Style TargetType="TextBlock" BasedOn="{StaticResource ListTableTextBlock}">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Direction}" Value="TX">
+ <Setter Property="Foreground" Value="#2196F3"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Direction}" Value="RX">
+ <Setter Property="Foreground" Value="#4CAF50"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBlock.Style>
+ </TextBlock>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="100" Header="鏉ユ簮">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Source}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+ </Grid>
+</UserControl>
diff --git a/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs b/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs
new file mode 100644
index 0000000..aa8a60b
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/CanDebugPanel.xaml.cs
@@ -0,0 +1,543 @@
+using Aging.Infrastructure.Drivers;
+using log4net;
+using Plugin.DeviceDebug.Entity;
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+
+namespace Plugin.DeviceDebug.Panels
+{
+ /// <summary>
+ /// CAN 甯ф樉绀烘ā鍨�
+ /// </summary>
+ public class CanFrameDisplay
+ {
+ public DateTime Timestamp { get; set; }
+ public string FrameId { get; set; } = string.Empty;
+ public int Dlc { get; set; }
+ public string Data { get; set; } = string.Empty;
+ public string Direction { get; set; } = string.Empty; // TX / RX
+ public string Source { get; set; } = string.Empty;
+ }
+
+ /// <summary>
+ /// CanDebugPanel.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class CanDebugPanel : UserControl
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(CanDebugPanel));
+
+ /// <summary>
+ /// 鏄惁涓虹淮鎶ゆā寮忥紙鎺у埗杈撳叆鍙敤鎬э級
+ /// </summary>
+ public static readonly DependencyProperty IsMaintenanceModeProperty =
+ DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(CanDebugPanel),
+ new PropertyMetadata(false));
+
+ public bool IsMaintenanceMode
+ {
+ get => (bool)GetValue(IsMaintenanceModeProperty);
+ set => SetValue(IsMaintenanceModeProperty, value);
+ }
+
+ /// <summary>
+ /// 鍛戒护鎵ц浜嬩欢
+ /// </summary>
+ public event EventHandler<DebugCommand>? OnCommandExecute;
+
+ /// <summary>
+ /// 鎺ユ敹甯у垪琛�
+ /// </summary>
+ public ObservableCollection<CanFrameDisplay> ReceivedFrames { get; } = new();
+
+ /// <summary>
+ /// CAN 椹卞姩閫傞厤鍣�
+ /// </summary>
+ private CanDriverAdapter? _driver;
+
+ /// <summary>
+ /// 杞瀹氭椂鍣紙鐢ㄤ簬妫�鏌ユ帴鏀堕偖绠憋級
+ /// </summary>
+ private DispatcherTimer? _pollTimer;
+
+ /// <summary>
+ /// 鏄惁宸茶繛鎺�
+ /// </summary>
+ private bool _isConnected;
+
+ /// <summary>
+ /// 璁惧ID锛堜粠 DeviceDebug 浼犲叆锛岄潰鏉胯嚜鍔ㄦ嫾鎺� deviceId锛�
+ /// </summary>
+ public string DeviceId
+ {
+ get => _deviceId;
+ set
+ {
+ _deviceId = value;
+ if (!string.IsNullOrEmpty(value))
+ {
+ // 灏嗚澶嘔D鍓嶇紑鎷兼帴鍒伴粯璁よ澶囨爣璇嗕腑锛岄伩鍏嶄笌妯℃嫙鍣ㄩ潰鏉块噸鍚�
+ UpdateDeviceIdDisplay();
+ }
+ }
+ }
+ private string _deviceId = string.Empty;
+
+ /// <summary>
+ /// 浠庤澶� Endpoint 瑙f瀽鍑虹殑鎬荤嚎鍚嶇О
+ /// </summary>
+ public string InitialBusName { get; set; }
+
+ /// <summary>
+ /// 浠庤澶� Endpoint 瑙f瀽鍑虹殑鑺傜偣缂栧彿
+ /// </summary>
+ public int? InitialNodeIndex { get; set; }
+
+ /// <summary>
+ /// 鏄惁鑷姩杩炴帴锛堜粠 DeviceDebug 鎵撳紑鏃�=true锛�
+ /// </summary>
+ public bool AutoConnectOnLoad { get; set; }
+
+ public CanDebugPanel()
+ {
+ InitializeComponent();
+ receiveList.ItemsSource = ReceivedFrames;
+ InitializePollTimer();
+ Loaded += CanDebugPanel_Loaded;
+ }
+
+ private void CanDebugPanel_Loaded(object sender, RoutedEventArgs e)
+ {
+ Loaded -= CanDebugPanel_Loaded;
+
+ // 浠庤澶囦俊鎭垵濮嬪寲鎬荤嚎/鑺傜偣杈撳叆
+ if (!string.IsNullOrEmpty(InitialBusName))
+ {
+ busNameInput.Text = InitialBusName;
+ }
+ if (InitialNodeIndex.HasValue)
+ {
+ nodeIndexInput.Text = InitialNodeIndex.Value.ToString();
+ }
+
+ // 鑷姩杩炴帴
+ if (AutoConnectOnLoad)
+ {
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ try { DoConnect(); }
+ catch (Exception ex) { log.Warn($"鑷姩杩炴帴澶辫触: {ex.Message}"); }
+ }), DispatcherPriority.Background);
+ }
+ }
+
+ private void UpdateDeviceIdDisplay()
+ {
+ if (!string.IsNullOrEmpty(_deviceId) && busNameInput != null)
+ {
+ // 淇濈暀榛樿鎬荤嚎鍚嶏紝浠呰褰� deviceId 浠ヤ緵鏃ュ織浣跨敤
+ log.Info($"[CanDebugPanel] 鍏宠仈璁惧: {_deviceId}");
+ }
+ }
+
+ /// <summary>
+ /// 鍒濆鍖栬疆璇㈠畾鏃跺櫒
+ /// </summary>
+ private void InitializePollTimer()
+ {
+ _pollTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(100) // 100ms 杞涓�娆�
+ };
+ _pollTimer.Tick += PollTimer_Tick;
+ }
+
+ /// <summary>
+ /// 杞瀹氭椂鍣ㄥ洖璋� - 浠呮洿鏂扮粺璁′俊鎭紙甯ф樉绀虹敱浜嬩欢椹卞姩锛�
+ /// </summary>
+ private void PollTimer_Tick(object? sender, EventArgs e)
+ {
+ if (!_isConnected || _driver == null) return;
+
+ try
+ {
+ // 鏇存柊缁熻
+ UpdateStats();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"杞缁熻鏇存柊澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 杩炴帴鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void ConnectBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ DoConnect();
+ }
+ catch (Exception ex)
+ {
+ log.Error($"CAN 杩炴帴澶辫触: {ex.Message}", ex);
+ MessageBox.Show($"杩炴帴澶辫触: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ UpdateConnectionStatus(false);
+ }
+ }
+
+ /// <summary>
+ /// 瀹為檯杩炴帴閫昏緫锛堜粠 UI 鎴栬嚜鍔ㄨ繛鎺ヨ皟鐢級
+ /// </summary>
+ private void DoConnect()
+ {
+ if (_isConnected) return;
+
+ string busName = busNameInput.Text.Trim();
+ if (!int.TryParse(nodeIndexInput.Text.Trim(), out int nodeIndex))
+ {
+ throw new ArgumentException("鑺傜偣缂栧彿鏍煎紡閿欒");
+ }
+
+ // 浼樺厛浣跨敤 DeviceDebug 浼犲叆鐨勮澶� ID锛屽惁鍒欒嚜鍔ㄧ敓鎴愪竴涓�
+ string deviceId = string.IsNullOrEmpty(_deviceId)
+ ? $"CAN_Test_{nodeIndex}_{DateTime.Now:HHmmss}"
+ : _deviceId;
+
+ _driver = new CanDriverAdapter(deviceId, busName, nodeIndex);
+
+ // 璁㈤槄椹卞姩鐨勫抚鎺ユ敹浜嬩欢锛堝嵆鏃舵樉绀烘帴鏀跺埌鐨勫抚锛�
+ _driver.DriverFrameReceived += OnDriverFrameReceived;
+
+ // 杩炴帴
+ _driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
+ _isConnected = true;
+
+ // 妯℃嫙鍣ㄦā寮忎笅锛岃繛鎺ュ悗鑷姩鍚敤缁存姢妯″紡锛堝厑璁稿彂閫侊級
+ IsMaintenanceMode = true;
+ if (maintenanceToggleBtn != null)
+ {
+ maintenanceToggleBtn.IsChecked = true;
+ maintenanceToggleBtn.IsEnabled = true;
+ }
+
+ // 鏇存柊 UI
+ UpdateConnectionStatus(true);
+ _pollTimer?.Start();
+
+ log.Info($"CAN 椹卞姩宸茶繛鎺�: Bus={busName}, Node={nodeIndex}, DeviceId={deviceId}");
+ }
+
+ /// <summary>
+ /// 椹卞姩甯ф帴鏀朵簨浠跺鐞� - 鍗虫椂鏄剧ず鎺ユ敹鍒扮殑甯�
+ /// </summary>
+ private void OnDriverFrameReceived(object? sender, CanSimulator.CanFrame frame)
+ {
+ try
+ {
+ // 鍒ゆ柇鏂瑰悜锛氬鏋滃抚鐨勬潵婧愭槸褰撳墠鑺傜偣锛屽垯涓鸿嚜鍔ㄥ搷搴旓紙鏄剧ず涓� RX锛�
+ string direction = "RX";
+ string source = frame.SourceNodeId;
+
+ AddReceivedFrame(
+ frame.FrameIdHex,
+ frame.DataHex,
+ direction,
+ source
+ );
+
+ log.Debug($"[鍗虫椂鏄剧ず] 鎺ユ敹甯�: {frame}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍗虫椂鏄剧ず甯уけ璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏂紑鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void DisconnectBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ _pollTimer?.Stop();
+ if (_driver != null)
+ {
+ _driver.DriverFrameReceived -= OnDriverFrameReceived;
+ _driver.DisconnectAsync().GetAwaiter().GetResult();
+ _driver.Dispose();
+ }
+ _driver = null;
+ _isConnected = false;
+ IsMaintenanceMode = false;
+
+ UpdateConnectionStatus(false);
+ log.Info("CAN 椹卞姩宸叉柇寮�");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"CAN 鏂紑澶辫触: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 娣锋潅妯″紡鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void PromiscuousBtn_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_isConnected || _driver == null) return;
+
+ try
+ {
+ _driver.QueryAsync("PROMISCUOUS", CancellationToken.None).GetAwaiter().GetResult();
+ log.Info("宸茶缃贩鏉傛ā寮忥紙鎺ユ敹鎵�鏈夊抚锛�");
+ MessageBox.Show("宸茶缃贩鏉傛ā寮忥紝灏嗘帴鏀舵墍鏈� CAN 甯�", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ log.Error($"璁剧疆娣锋潅妯″紡澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 缁存姢妯″紡鍒囨崲鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void MaintenanceToggleBtn_Click(object sender, RoutedEventArgs e)
+ {
+ bool isOn = maintenanceToggleBtn.IsChecked == true;
+ IsMaintenanceMode = isOn;
+
+ if (isOn)
+ {
+ maintenanceToggleBtn.Content = "缁存姢妯″紡: 寮�";
+ maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
+ log.Info("缁存姢妯″紡宸插紑鍚紙鍙彂閫� CAN 甯э級");
+ }
+ else
+ {
+ maintenanceToggleBtn.Content = "缁存姢妯″紡: 鍏�";
+ maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E"));
+ log.Info("缁存姢妯″紡宸插叧闂紙鍙鐩戞帶锛�");
+ }
+ }
+
+ /// <summary>
+ /// 鑷姩鍝嶅簲妯″紡鍒囨崲鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void AutoResponseToggleBtn_Click(object sender, RoutedEventArgs e)
+ {
+ if (!_isConnected || _driver == null) return;
+
+ try
+ {
+ bool isOn = autoResponseToggleBtn.IsChecked == true;
+
+ // 閫氳繃椹卞姩鍙戦�佸懡浠ゅ惎鐢�/绂佺敤鑷姩鍝嶅簲
+ string cmd = isOn ? "AUTO_RESPONSE:ON" : "AUTO_RESPONSE:OFF";
+ _driver.QueryAsync(cmd, CancellationToken.None).GetAwaiter().GetResult();
+
+ if (isOn)
+ {
+ autoResponseToggleBtn.Content = "鑷姩鍝嶅簲: 寮�";
+ autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9800"));
+ log.Info("鑷姩鍝嶅簲妯″紡宸插紑鍚紙鏀跺埌甯у皢鑷姩鍥炲锛�");
+ }
+ else
+ {
+ autoResponseToggleBtn.Content = "鑷姩鍝嶅簲: 鍏�";
+ autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E"));
+ log.Info("鑷姩鍝嶅簲妯″紡宸插叧闂�");
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍒囨崲鑷姩鍝嶅簲妯″紡澶辫触: {ex.Message}");
+ MessageBox.Show($"鍒囨崲澶辫触: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊杩炴帴鐘舵�佹樉绀�
+ /// </summary>
+ private void UpdateConnectionStatus(bool connected)
+ {
+ _isConnected = connected;
+ if (connected)
+ {
+ statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
+ statusText.Text = "宸茶繛鎺�";
+ connectBtn.IsEnabled = false;
+ disconnectBtn.IsEnabled = true;
+ promiscuousBtn.IsEnabled = true;
+ maintenanceToggleBtn.IsEnabled = true;
+ autoResponseToggleBtn.IsEnabled = true;
+ busNameInput.IsEnabled = false;
+ nodeIndexInput.IsEnabled = false;
+ }
+ else
+ {
+ statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E"));
+ statusText.Text = "鏈繛鎺�";
+ connectBtn.IsEnabled = true;
+ disconnectBtn.IsEnabled = false;
+ promiscuousBtn.IsEnabled = false;
+ maintenanceToggleBtn.IsEnabled = false;
+ autoResponseToggleBtn.IsEnabled = false;
+ busNameInput.IsEnabled = true;
+ nodeIndexInput.IsEnabled = true;
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊缁熻淇℃伅
+ /// </summary>
+ private void UpdateStats()
+ {
+ if (_driver == null) return;
+
+ try
+ {
+ var status = _driver.GetStatus();
+ var response = _driver.QueryAsync("STATUS", CancellationToken.None).GetAwaiter().GetResult();
+ // 瑙f瀽鐘舵��: "STATUS:Node=..., Active=True, Rx=0, Tx=0, Err=0, Mailbox=0|Bus=..."
+ statsText.Text = $"TX: {GetStatValue(response, "Tx")} RX: {GetStatValue(response, "Rx")} Err: {GetStatValue(response, "Err")}";
+ }
+ catch
+ {
+ // 蹇界暐缁熻鏇存柊澶辫触
+ }
+ }
+
+ /// <summary>
+ /// 浠庣姸鎬佸瓧绗︿覆涓彁鍙栨暟鍊煎��
+ /// </summary>
+ private string GetStatValue(string statusStr, string key)
+ {
+ try
+ {
+ var parts = statusStr.Split(',');
+ foreach (var part in parts)
+ {
+ var kv = part.Trim().Split('=');
+ if (kv.Length == 2 && kv[0].Trim() == key)
+ {
+ return kv[1].Trim();
+ }
+ }
+ }
+ catch { }
+ return "0";
+ }
+
+ /// <summary>
+ /// 娣诲姞鎺ユ敹甯�
+ /// </summary>
+ public void AddReceivedFrame(string frameId, string data, string direction = "RX", string source = "")
+ {
+ try
+ {
+ var frame = new CanFrameDisplay
+ {
+ Timestamp = DateTime.Now,
+ FrameId = frameId,
+ Dlc = data.Split(new[] { ' ', '-' }, StringSplitOptions.RemoveEmptyEntries).Length,
+ Data = data,
+ Direction = direction,
+ Source = source
+ };
+
+ Application.Current.Dispatcher.Invoke(() =>
+ {
+ ReceivedFrames.Insert(0, frame);
+
+ // 闄愬埗鏄剧ず鏁伴噺
+ if (ReceivedFrames.Count > 500)
+ {
+ var toRemove = ReceivedFrames.Skip(500).ToList();
+ foreach (var item in toRemove)
+ {
+ ReceivedFrames.Remove(item);
+ }
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ log.Error($"娣诲姞鎺ユ敹甯уけ璐�: {ex.Message}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鍙戦�佹寜閽偣鍑�
+ /// </summary>
+ private void SendBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (!IsMaintenanceMode)
+ {
+ MessageBox.Show("褰撳墠涓哄彧璇绘ā寮忥紝鏃犳硶鍙戦�佹暟鎹�", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (!_isConnected || _driver == null)
+ {
+ MessageBox.Show("璇峰厛杩炴帴 CAN 鎬荤嚎", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ string frameIdStr = frameIdInput.Text.Trim();
+ string dataStr = dataInput.Text.Trim();
+
+ // 楠岃瘉甯� ID
+ uint frameId = CanSimulator.CanFrame.ParseFrameId(frameIdStr);
+
+ // 楠岃瘉鏁版嵁
+ byte[] dataBytes = CanSimulator.CanFrame.ParseData(dataStr);
+ if (dataBytes.Length > 8)
+ {
+ MessageBox.Show("鏁版嵁闀垮害涓嶈兘瓒呰繃 8 瀛楄妭", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ // 閫氳繃椹卞姩鍙戦��
+ string command = $"SEND:{frameIdStr}:{dataStr}";
+ var response = _driver.SendCommandAsync(command, CancellationToken.None).GetAwaiter().GetResult();
+
+ // 娣诲姞鍒版樉绀哄垪琛紙TX锛�
+ AddReceivedFrame($"0x{frameId:X3}", BitConverter.ToString(dataBytes).Replace("-", " "), "TX", "LOCAL");
+
+ log.Info($"鍙戦�� CAN 甯�: ID=0x{frameId:X3}, Data={BitConverter.ToString(dataBytes)}, Response={response}");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鍙戦�� CAN 甯уけ璐�: {ex.Message}", ex);
+ MessageBox.Show($"鍙戦�佸け璐�: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ /// <summary>
+ /// 娓呯┖鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void ClearBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ ReceivedFrames.Clear();
+ log.Info("娓呯┖ CAN 鎺ユ敹璁板綍");
+ }
+ catch (Exception ex)
+ {
+ log.Error($"娓呯┖鎺ユ敹璁板綍澶辫触: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml b/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml
new file mode 100644
index 0000000..aa46097
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml
@@ -0,0 +1,176 @@
+<UserControl x:Class="Plugin.DeviceDebug.Panels.PlcDebugPanel"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ mc:Ignorable="d"
+ d:DesignHeight="500" d:DesignWidth="600">
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid dp:GridHelper.Space="12" Margin="12">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 鏍囬 -->
+ <Border Grid.Row="0" Background="#9C27B0" CornerRadius="4" Padding="12,8">
+ <TextBlock Text="PLC 璋冭瘯闈㈡澘" FontSize="16" FontWeight="Bold" Foreground="White"/>
+ </Border>
+
+ <!-- IO 鎺у埗鍖� -->
+ <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12">
+ <Grid dp:GridHelper.Space="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="80"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 鍦板潃 -->
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="鍦板潃:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="addressInput"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4" Text="M0.0"/>
+
+ <!-- 鍊� -->
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="鍊�:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <ComboBox Grid.Row="1" Grid.Column="1" x:Name="valueSelect"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4">
+ <ComboBoxItem Content="True" IsSelected="True"/>
+ <ComboBoxItem Content="False"/>
+ </ComboBox>
+
+ <!-- 鍐欏叆鎸夐挳 -->
+ <Button Grid.Row="2" Grid.ColumnSpan="2" x:Name="writeBtn" Content="鍐欏叆" Padding="20,6"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ Click="WriteBtn_Click" HorizontalAlignment="Left"
+ Background="#9C27B0" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ </Grid>
+ </Border>
+
+ <!-- IO 鐘舵�佹樉绀哄尯 -->
+ <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Background="#F5F5F5" Padding="8,6">
+ <TextBlock Text="IO 鐐逛綅鐘舵��" FontWeight="Bold" FontSize="14"/>
+ </Border>
+
+ <ListView Grid.Row="1" x:Name="ioStatusList" Height="180"
+ Style="{StaticResource DefaultListTable}"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Standard">
+ <ListView.View>
+ <GridView AllowsColumnReorder="False">
+ <GridViewColumn Width="120" Header="鍦板潃">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding Address}" FontFamily="Consolas"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="80" Header="鍊�">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Text="{Binding Value}">
+ <TextBlock.Style>
+ <Style TargetType="TextBlock" BasedOn="{StaticResource ListTableTextBlock}">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Value}" Value="True">
+ <Setter Property="Foreground" Value="#4CAF50"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Value}" Value="False">
+ <Setter Property="Foreground" Value="#999"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBlock.Style>
+ </TextBlock>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ <GridViewColumn Width="150" Header="鏇存柊鏃堕棿">
+ <GridViewColumn.CellTemplate>
+ <DataTemplate>
+ <Border Style="{StaticResource CellBorder}">
+ <TextBlock Style="{StaticResource ListTableTextBlock}"
+ Text="{Binding LastUpdate, StringFormat=HH:mm:ss}"/>
+ </Border>
+ </DataTemplate>
+ </GridViewColumn.CellTemplate>
+ </GridViewColumn>
+ </GridView>
+ </ListView.View>
+ </ListView>
+ </Grid>
+ </Border>
+
+ <!-- 浜掗攣鐘舵�佸尯 -->
+ <Border Grid.Row="3" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <Border Grid.Row="0" Background="#FFF3E0" Padding="8,6">
+ <TextBlock Text="浜掗攣鐘舵��" FontWeight="Bold" FontSize="14" Foreground="#E65100"/>
+ </Border>
+
+ <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
+ <ItemsControl x:Name="interlockList" Margin="8">
+ <ItemsControl.ItemTemplate>
+ <DataTemplate>
+ <Border BorderBrush="#EEE" BorderThickness="0,0,0,1" Padding="4,6">
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Text="{Binding Name}" FontWeight="Bold" Width="150"/>
+ <TextBlock Text="{Binding Status}" Margin="10,0,0,0">
+ <TextBlock.Style>
+ <Style TargetType="TextBlock">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="婊¤冻">
+ <Setter Property="Foreground" Value="#4CAF50"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="涓嶆弧瓒�">
+ <Setter Property="Foreground" Value="#F44336"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </TextBlock.Style>
+ </TextBlock>
+ </StackPanel>
+ </Border>
+ </DataTemplate>
+ </ItemsControl.ItemTemplate>
+ </ItemsControl>
+ </ScrollViewer>
+ </Grid>
+ </Border>
+ </Grid>
+</UserControl>
diff --git a/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs b/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs
new file mode 100644
index 0000000..00b58b0
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/PlcDebugPanel.xaml.cs
@@ -0,0 +1,197 @@
+using log4net;
+using Plugin.DeviceDebug.Entity;
+using System;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Plugin.DeviceDebug.Panels
+{
+ /// <summary>
+ /// IO 鐐逛綅鐘舵�佹樉绀烘ā鍨�
+ /// </summary>
+ public class IoPointStatus
+ {
+ public string Address { get; set; }
+ public bool Value { get; set; }
+ public DateTime LastUpdate { get; set; }
+ }
+
+ /// <summary>
+ /// 浜掗攣鐘舵�佹樉绀烘ā鍨�
+ /// </summary>
+ public class InterlockStatus
+ {
+ public string Name { get; set; }
+ public string Status { get; set; } // "婊¤冻" / "涓嶆弧瓒�"
+ }
+
+ /// <summary>
+ /// PlcDebugPanel.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class PlcDebugPanel : UserControl
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(PlcDebugPanel));
+
+ /// <summary>
+ /// 鏄惁涓虹淮鎶ゆā寮忥紙鎺у埗杈撳叆鍙敤鎬э級
+ /// </summary>
+ public static readonly DependencyProperty IsMaintenanceModeProperty =
+ DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(PlcDebugPanel),
+ new PropertyMetadata(false));
+
+ public bool IsMaintenanceMode
+ {
+ get => (bool)GetValue(IsMaintenanceModeProperty);
+ set => SetValue(IsMaintenanceModeProperty, value);
+ }
+
+ /// <summary>
+ /// 鍛戒护鎵ц浜嬩欢
+ /// </summary>
+ public event EventHandler<DebugCommand> OnCommandExecute;
+
+ /// <summary>
+ /// IO 鐐逛綅鐘舵�佸垪琛�
+ /// </summary>
+ public ObservableCollection<IoPointStatus> IoStatuses { get; } = new();
+
+ /// <summary>
+ /// 浜掗攣鐘舵�佸垪琛�
+ /// </summary>
+ public ObservableCollection<InterlockStatus> Interlocks { get; } = new();
+
+ public PlcDebugPanel()
+ {
+ InitializeComponent();
+ ioStatusList.ItemsSource = IoStatuses;
+ interlockList.ItemsSource = Interlocks;
+
+ // 鍒濆鍖栫ず渚嬫暟鎹�
+ InitSampleData();
+ }
+
+ /// <summary>
+ /// 鍒濆鍖栫ず渚嬫暟鎹紙瀹為檯搴斾粠 PLC 璇诲彇锛�
+ /// </summary>
+ private void InitSampleData()
+ {
+ try
+ {
+ // 娣诲姞绀轰緥 IO 鐐逛綅
+ IoStatuses.Add(new IoPointStatus { Address = "M0.0", Value = false, LastUpdate = DateTime.Now });
+ IoStatuses.Add(new IoPointStatus { Address = "M0.1", Value = true, LastUpdate = DateTime.Now });
+ IoStatuses.Add(new IoPointStatus { Address = "M0.2", Value = false, LastUpdate = DateTime.Now });
+ IoStatuses.Add(new IoPointStatus { Address = "I0.0", Value = true, LastUpdate = DateTime.Now });
+ IoStatuses.Add(new IoPointStatus { Address = "Q0.0", Value = false, LastUpdate = DateTime.Now });
+
+ // 娣诲姞绀轰緥浜掗攣
+ Interlocks.Add(new InterlockStatus { Name = "瀹夊叏闂ㄥ叧闂�", Status = "婊¤冻" });
+ Interlocks.Add(new InterlockStatus { Name = "鎬ュ仠鏈Е鍙�", Status = "婊¤冻" });
+ Interlocks.Add(new InterlockStatus { Name = "姘斿帇姝e父", Status = "涓嶆弧瓒�" });
+ Interlocks.Add(new InterlockStatus { Name = "娓╁害姝e父", Status = "婊¤冻" });
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍒濆鍖栫ず渚嬫暟鎹け璐�", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊 IO 鐐逛綅鐘舵��
+ /// </summary>
+ public void UpdateIoStatus(string address, bool value)
+ {
+ try
+ {
+ var existing = IoStatuses.FirstOrDefault(s => s.Address == address);
+ if (existing != null)
+ {
+ existing.Value = value;
+ existing.LastUpdate = DateTime.Now;
+ }
+ else
+ {
+ IoStatuses.Add(new IoPointStatus
+ {
+ Address = address,
+ Value = value,
+ LastUpdate = DateTime.Now
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鏇存柊 IO 鐘舵�佸け璐�: {address}", ex);
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊浜掗攣鐘舵��
+ /// </summary>
+ public void UpdateInterlock(string name, bool satisfied)
+ {
+ try
+ {
+ var existing = Interlocks.FirstOrDefault(i => i.Name == name);
+ if (existing != null)
+ {
+ existing.Status = satisfied ? "婊¤冻" : "涓嶆弧瓒�";
+ }
+ else
+ {
+ Interlocks.Add(new InterlockStatus
+ {
+ Name = name,
+ Status = satisfied ? "婊¤冻" : "涓嶆弧瓒�"
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Error($"鏇存柊浜掗攣鐘舵�佸け璐�: {name}", ex);
+ }
+ }
+
+ private void WriteBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (!IsMaintenanceMode)
+ {
+ MessageBox.Show("褰撳墠涓哄彧璇绘ā寮忥紝鏃犳硶鎵ц鍐欏叆鎿嶄綔", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ string address = addressInput.Text.Trim();
+ if (string.IsNullOrEmpty(address))
+ {
+ MessageBox.Show("璇疯緭鍏� IO 鍦板潃", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ bool value = (valueSelect.SelectedItem as ComboBoxItem)?.Content?.ToString() == "True";
+
+ // 鍒涘缓鍛戒护
+ var command = new PlcWriteCommand
+ {
+ IoAddress = address,
+ Value = value
+ };
+
+ // 瑙﹀彂鍛戒护鎵ц浜嬩欢
+ OnCommandExecute?.Invoke(this, command);
+
+ // 鏇存柊鏈湴鏄剧ず
+ UpdateIoStatus(address, value);
+
+ log.Info($"鍐欏叆 PLC IO: {address} = {value}");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍐欏叆 PLC IO 澶辫触", ex);
+ MessageBox.Show($"鍐欏叆澶辫触: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ }
+}
diff --git a/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml b/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml
new file mode 100644
index 0000000..213d4c9
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml
@@ -0,0 +1,117 @@
+<UserControl x:Class="Plugin.DeviceDebug.Panels.PowerSupplyDebugPanel"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ mc:Ignorable="d"
+ d:DesignHeight="500" d:DesignWidth="600">
+
+ <Grid dp:GridHelper.Space="12" Margin="12">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 鏍囬 -->
+ <Border Grid.Row="0" Background="#2196F3" CornerRadius="4" Padding="12,8">
+ <TextBlock Text="鐢垫簮鎺у埗闈㈡澘" FontSize="16" FontWeight="Bold" Foreground="White"/>
+ </Border>
+
+ <!-- 璁剧疆鍖� -->
+ <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12">
+ <Grid dp:GridHelper.Space="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="*"/>
+ <ColumnDefinition Width="50"/>
+ </Grid.ColumnDefinitions>
+
+ <!-- 鐢靛帇璁惧畾 -->
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="鐢靛帇璁惧畾:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <TextBox Grid.Row="0" Grid.Column="1" x:Name="voltageInput"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4" Text="0.00"/>
+ <TextBlock Grid.Row="0" Grid.Column="2" Text="V" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#666"/>
+
+ <!-- 闄愭祦璁惧畾 -->
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="闄愭祦璁惧畾:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <TextBox Grid.Row="1" Grid.Column="1" x:Name="currentInput"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ VerticalContentAlignment="Center" Padding="6,4" Text="0.000"/>
+ <TextBlock Grid.Row="1" Grid.Column="2" Text="A" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#666"/>
+
+ <!-- 杈撳嚭鎺у埗 -->
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="杈撳嚭鎺у埗:" VerticalAlignment="Center" FontWeight="Bold"/>
+ <ToggleButton Grid.Row="2" Grid.Column="1" x:Name="outputToggle"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ Checked="OutputToggle_Changed" Unchecked="OutputToggle_Changed"
+ Height="30" Cursor="Hand">
+ <ToggleButton.Style>
+ <Style TargetType="ToggleButton">
+ <Setter Property="Content" Value="OFF"/>
+ <Setter Property="Background" Value="#F44336"/>
+ <Setter Property="Foreground" Value="White"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Style.Triggers>
+ <Trigger Property="IsChecked" Value="True">
+ <Setter Property="Content" Value="ON"/>
+ <Setter Property="Background" Value="#4CAF50"/>
+ </Trigger>
+ </Style.Triggers>
+ </Style>
+ </ToggleButton.Style>
+ </ToggleButton>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <StackPanel Grid.Row="3" Grid.ColumnSpan="3" Orientation="Horizontal"
+ dp:StackPanelHelper.ChildSpace="10" HorizontalAlignment="Left" Margin="0,6,0,0">
+ <Button x:Name="applyBtn" Content="璁剧疆" Padding="16,6"
+ IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}"
+ Click="ApplyBtn_Click"
+ Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ <Button x:Name="measureBtn" Content="娴嬮噺" Padding="16,6"
+ Click="MeasureBtn_Click"
+ Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+
+ <!-- 瀹炴祴鍊兼樉绀哄尯 -->
+ <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12">
+ <Grid dp:GridHelper.Space="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="100"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="瀹炴祴鐢靛帇:" FontWeight="Bold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="0" Grid.Column="1" x:Name="measuredVoltage"
+ Text="-- V" FontSize="18" Foreground="#2196F3" VerticalAlignment="Center"/>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="瀹炴祴鐢垫祦:" FontWeight="Bold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="1" Grid.Column="1" x:Name="measuredCurrent"
+ Text="-- A" FontSize="18" Foreground="#FF9800" VerticalAlignment="Center"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="鏇存柊鏃堕棿:" FontWeight="Bold" VerticalAlignment="Center"/>
+ <TextBlock Grid.Row="2" Grid.Column="1" x:Name="lastUpdateTime"
+ Text="--" Foreground="#999" VerticalAlignment="Center"/>
+ </Grid>
+ </Border>
+
+
+ </Grid>
+</UserControl>
diff --git a/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs b/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs
new file mode 100644
index 0000000..fa76700
--- /dev/null
+++ b/Plugin.DeviceDebug/Panels/PowerSupplyDebugPanel.xaml.cs
@@ -0,0 +1,125 @@
+using log4net;
+using Plugin.DeviceDebug.Entity;
+using System;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Plugin.DeviceDebug.Panels
+{
+ /// <summary>
+ /// PowerSupplyDebugPanel.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class PowerSupplyDebugPanel : UserControl
+ {
+ private static readonly ILog log = LogManager.GetLogger(typeof(PowerSupplyDebugPanel));
+
+ /// <summary>
+ /// 鏄惁涓虹淮鎶ゆā寮忥紙鎺у埗杈撳叆鍙敤鎬э級
+ /// </summary>
+ public static readonly DependencyProperty IsMaintenanceModeProperty =
+ DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(PowerSupplyDebugPanel),
+ new PropertyMetadata(false));
+
+ public bool IsMaintenanceMode
+ {
+ get => (bool)GetValue(IsMaintenanceModeProperty);
+ set => SetValue(IsMaintenanceModeProperty, value);
+ }
+
+ /// <summary>
+ /// 鍛戒护鎵ц浜嬩欢
+ /// </summary>
+ public event EventHandler<DebugCommand> OnCommandExecute;
+
+ public PowerSupplyDebugPanel()
+ {
+ InitializeComponent();
+ }
+
+ /// <summary>
+ /// 鏇存柊瀹炴祴鍊�
+ /// </summary>
+ public void UpdateReadback(string voltage, string current, DateTime time)
+ {
+ try
+ {
+ measuredVoltage.Text = $"{voltage} V";
+ measuredCurrent.Text = $"{current} A";
+ lastUpdateTime.Text = time.ToString("yyyy-MM-dd HH:mm:ss");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鏇存柊瀹炴祴鍊煎け璐�", ex);
+ }
+ }
+
+ private void ApplyBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (!IsMaintenanceMode)
+ {
+ MessageBox.Show("褰撳墠涓哄彧璇绘ā寮忥紝鏃犳硶鎵ц璁剧疆鎿嶄綔", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // 瑙f瀽杈撳叆鍊�
+ if (!double.TryParse(voltageInput.Text, out double voltage))
+ {
+ MessageBox.Show("鐢靛帇鍊兼牸寮忎笉姝g‘", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ if (!double.TryParse(currentInput.Text, out double currentLimit))
+ {
+ MessageBox.Show("鐢垫祦鍊兼牸寮忎笉姝g‘", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ bool output = outputToggle.IsChecked == true;
+
+ // 鍒涘缓鍛戒护
+ var command = new PowerSetCommand
+ {
+ Voltage = voltage,
+ CurrentLimit = currentLimit,
+ Output = output
+ };
+
+ // 瑙﹀彂鍛戒护鎵ц浜嬩欢
+ OnCommandExecute?.Invoke(this, command);
+
+ log.Info($"鍙戦�佺數婧愯缃懡浠�: V={voltage}, I={currentLimit}, Output={output}");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍙戦�佺數婧愯缃懡浠ゅけ璐�", ex);
+ MessageBox.Show($"鍙戦�佸懡浠ゅけ璐�: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void MeasureBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // 鍒涘缓娴嬮噺鍛戒护
+ var command = new PowerMeasureCommand();
+
+ // 瑙﹀彂鍛戒护鎵ц浜嬩欢
+ OnCommandExecute?.Invoke(this, command);
+
+ log.Info("鍙戦�佺數婧愭祴閲忓懡浠�");
+ }
+ catch (Exception ex)
+ {
+ log.Error("鍙戦�佺數婧愭祴閲忓懡浠ゅけ璐�", ex);
+ MessageBox.Show($"鍙戦�佸懡浠ゅけ璐�: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void OutputToggle_Changed(object sender, RoutedEventArgs e)
+ {
+ // ToggleButton 鐘舵�佸彉鍖栨椂鐨勮瑙夊弽棣堝凡鍦� XAML Style 涓鐞�
+ }
+ }
+}
diff --git a/Plugin.DeviceDebug/Plugin.DeviceDebug.csproj b/Plugin.DeviceDebug/Plugin.DeviceDebug.csproj
new file mode 100644
index 0000000..64faa22
--- /dev/null
+++ b/Plugin.DeviceDebug/Plugin.DeviceDebug.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ <PackageReference Include="log4net" Version="3.0.4" />
+ <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+ <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" />
+ <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
+ <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
+ <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
+ <ProjectReference Include="..\Aging.Infrastructure\Aging.Infrastructure.csproj" />
+ <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugin.IntegrationTest/App.xaml b/Plugin.IntegrationTest/App.xaml
new file mode 100644
index 0000000..ef6d0b7
--- /dev/null
+++ b/Plugin.IntegrationTest/App.xaml
@@ -0,0 +1,36 @@
+<Application x:Class="Plugin.IntegrationTest.App"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
+ xmlns:hc="clr-namespace:HMI.Core;assembly=HMI.Core"
+ StartupUri="MainWindow.xaml"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib">
+ <Application.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/BaseWindow.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Style/FormStyle.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Resources/Font/Font.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Style/Colors.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonResource.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Style/MsgBoxStyle.xaml"/>
+ <ResourceDictionary Source="/HMI.Base;component/MaskControl.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ <FontFamily x:Key="iconfont">
+ /HMI.UI;component/Resources/Font/#iconfont
+ </FontFamily>
+ <sys:Double x:Key="nanD">NaN</sys:Double>
+ <Style TargetType="{x:Type pu:FormGroup}" BasedOn="{StaticResource InputFormGroupStyle}"></Style>
+ <uic:PathImageConverter x:Key="path2Image"/>
+ <uic:DataGridRowNumberConverter x:Key="rowNumber"/>
+ <uic:VisibilityBoolConverter x:Key="vbConverter"/>
+ <uic:DataGridRowFlagConverter x:Key="rowFlag"/>
+ <uic:ReverseBoolConverter x:Key="reverseBool"/>
+ <hc:LangConverter x:Key="lc"/>
+ <hc:WidthSizeConverter x:Key="fc"/>
+ <hc:HeightSizeConverter x:Key="hfc"/>
+ </ResourceDictionary>
+ </Application.Resources>
+</Application>
diff --git a/Plugin.IntegrationTest/App.xaml.cs b/Plugin.IntegrationTest/App.xaml.cs
new file mode 100644
index 0000000..4fe3c50
--- /dev/null
+++ b/Plugin.IntegrationTest/App.xaml.cs
@@ -0,0 +1,8 @@
+using System.Windows;
+
+namespace Plugin.IntegrationTest
+{
+ public partial class App : Application
+ {
+ }
+}
diff --git a/Plugin.IntegrationTest/MainWindow.xaml b/Plugin.IntegrationTest/MainWindow.xaml
new file mode 100644
index 0000000..3e3aece
--- /dev/null
+++ b/Plugin.IntegrationTest/MainWindow.xaml
@@ -0,0 +1,136 @@
+<Window x:Class="Plugin.IntegrationTest.MainWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ Title="浠櫒浠〃娉ㄥ唽 & 纭欢璋冭瘯 - 闆嗘垚娴嬭瘯鐜"
+ Height="800" Width="1300"
+ WindowStartupLocation="CenterScreen"
+ WindowState="Maximized"
+ Background="#F5F5F5"
+ Loaded="Window_Loaded" Closing="Window_Closing">
+ <Grid Margin="8">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!-- 椤堕儴鐘舵�佹爮 -->
+ <Border Grid.Row="0" Background="#E3F2FD" CornerRadius="4" Padding="12,6" Margin="0,0,0,6">
+ <DockPanel>
+ <TextBlock Text="闆嗘垚娴嬭瘯鐜" FontSize="16" FontWeight="Bold"
+ VerticalAlignment="Center" Foreground="#1565C0"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
+ <TextBlock x:Name="txtStatus" Text="灏辩华" FontSize="13"
+ VerticalAlignment="Center" Foreground="#666" Margin="0,0,12,0"/>
+ <Button x:Name="btnRun" Content="鈻� 杩愯鑷姩鍖栨祴璇�" Width="150" Height="32"
+ FontSize="13" FontWeight="SemiBold"
+ Background="#4CAF50" Foreground="White" BorderThickness="0"
+ Cursor="Hand" Click="BtnRun_Click" Margin="0,0,6,0"/>
+ <Button x:Name="btnReset" Content="閲嶇疆鏁版嵁" Width="90" Height="32"
+ FontSize="13" Click="BtnReset_Click" Margin="0,0,6,0"
+ Background="#FF9800" Foreground="White" BorderThickness="0" Cursor="Hand"/>
+ <Button x:Name="btnClear" Content="娓呯┖鏃ュ織" Width="80" Height="32"
+ FontSize="13" Click="BtnClear_Click"/>
+ </StackPanel>
+ </DockPanel>
+ </Border>
+
+ <!-- 涓� TabControl -->
+ <TabControl x:Name="mainTabs" Grid.Row="1" Margin="0,4">
+ <!-- Tab 1: 鑷姩鍖栨祴璇� -->
+ <TabItem Header=" 鑷姩鍖栨祴璇� " FontSize="13">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <ProgressBar x:Name="progressBar" Height="6" Minimum="0" Maximum="100" Value="0" Margin="4"/>
+ <TextBox x:Name="txtLog" Grid.Row="1"
+ IsReadOnly="True" VerticalScrollBarVisibility="Auto"
+ HorizontalScrollBarVisibility="Auto"
+ FontFamily="Consolas" FontSize="12.5"
+ Background="#1E1E1E" Foreground="#D4D4D4"
+ BorderThickness="1" BorderBrush="#CCC"
+ Padding="8" TextWrapping="NoWrap"
+ AcceptsReturn="True" VerticalContentAlignment="Top"/>
+ </Grid>
+ </TabItem>
+
+ <!-- Tab 2: 浠櫒浠〃娉ㄥ唽 -->
+ <TabItem Header=" 浠櫒浠〃娉ㄥ唽 " FontSize="13">
+ <Border x:Name="deviceConfigHost" Background="White"/>
+ </TabItem>
+
+ <!-- Tab 3: 纭欢璋冭瘯 -->
+ <TabItem Header=" 纭欢璋冭瘯 " FontSize="13">
+ <Border x:Name="deviceDebugHost" Background="White"/>
+ </TabItem>
+
+ <!-- Tab 4: CAN 妯℃嫙鍣� -->
+ <TabItem Header=" CAN 妯℃嫙鍣� " FontSize="13">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <!-- 妯℃嫙鍣ㄦ帶鍒堕潰鏉� -->
+ <Border Grid.Row="0" Background="#FFF3E0" CornerRadius="4" Padding="12,8" Margin="4">
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition Width="*"/>
+ </Grid.ColumnDefinitions>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <TextBlock Grid.Row="0" Grid.Column="0" Text="鎬荤嚎鐘舵��:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,4"/>
+ <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
+ <Ellipse x:Name="canBusIndicator" Width="14" Height="14" Fill="#9E9E9E" Margin="0,0,8,0"/>
+ <TextBlock x:Name="canBusStatusText" Text="鏈惎鍔�" FontSize="13"/>
+ </StackPanel>
+
+ <TextBlock Grid.Row="1" Grid.Column="0" Text="鎬荤嚎鍚嶇О:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,4"/>
+ <TextBox Grid.Row="1" Grid.Column="1" x:Name="canBusNameInput" Text="CAN_BUS_1"
+ VerticalContentAlignment="Center" Padding="6,4" Margin="0,0,80,4" Width="200" HorizontalAlignment="Left"/>
+
+ <TextBlock Grid.Row="2" Grid.Column="0" Text="鎺у埗:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,0"/>
+ <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
+ <Button x:Name="canStartBtn" Content="鍚姩鎬荤嚎" Width="100" Height="30" Margin="0,0,8,0"
+ Click="CanStartBtn_Click" Background="#4CAF50" Foreground="White"
+ BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+ <Button x:Name="canStopBtn" Content="鍋滄鎬荤嚎" Width="100" Height="30" Margin="0,0,8,0"
+ Click="CanStopBtn_Click" Background="#F44336" Foreground="White"
+ BorderThickness="0" Cursor="Hand" FontWeight="SemiBold" IsEnabled="False"/>
+ <Button x:Name="canResetBtn" Content="閲嶇疆鎵�鏈�" Width="100" Height="30"
+ Click="CanResetBtn_Click" Background="#9E9E9E" Foreground="White"
+ BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+
+ <!-- CAN 璋冭瘯闈㈡澘 -->
+ <Border x:Name="canSimulatorHost" Grid.Row="1" Background="White" Margin="4"/>
+ </Grid>
+ </TabItem>
+ </TabControl>
+
+ <!-- 搴曢儴缁撴灉鏍� -->
+ <Border Grid.Row="2" Padding="12,6" Background="#FAFAFA"
+ BorderBrush="#DDD" BorderThickness="1" CornerRadius="4">
+ <DockPanel>
+ <TextBlock x:Name="txtSummary" FontSize="13" FontWeight="SemiBold"
+ VerticalAlignment="Center" Foreground="#333" Text="灏辩华 - 璇峰垏鎹㈠埌瀵瑰簲 Tab 杩涜鎵嬪姩娴嬭瘯"/>
+ <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
+ <TextBlock x:Name="txtPassed" FontSize="13" Foreground="#4CAF50"
+ Margin="0,0,16,0" VerticalAlignment="Center"/>
+ <TextBlock x:Name="txtFailed" FontSize="13" Foreground="#F44336"
+ VerticalAlignment="Center"/>
+ </StackPanel>
+ </DockPanel>
+ </Border>
+ </Grid>
+</Window>
diff --git a/Plugin.IntegrationTest/MainWindow.xaml.cs b/Plugin.IntegrationTest/MainWindow.xaml.cs
new file mode 100644
index 0000000..3e80e2d
--- /dev/null
+++ b/Plugin.IntegrationTest/MainWindow.xaml.cs
@@ -0,0 +1,840 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Threading;
+using Aging.Infrastructure.Drivers;
+using CanSimulator;
+using log4net;
+using Plugin.DeviceConfig.Entity;
+using Plugin.DeviceConfig.Model;
+using Plugin.DeviceDebug.Entity;
+using Plugin.DeviceDebug.Model;
+using Plugin.DeviceDebug.Panels;
+
+namespace Plugin.IntegrationTest
+{
+ public partial class MainWindow : Window
+ {
+ private int _passed;
+ private int _failed;
+ private int _totalSteps;
+ private int _currentStep;
+ private string _testDir = string.Empty;
+ private string _logFilePath = string.Empty;
+ private readonly StringBuilder _logBuffer = new();
+
+ // 宓屽叆鐨勬彃浠跺疄渚�
+ private Plugin.DeviceConfig.DeviceConfig? _deviceConfigPlugin;
+ private Plugin.DeviceDebug.DeviceDebug? _deviceDebugPlugin;
+ private static readonly ILog _testLog = LogManager.GetLogger("IntegrationTest");
+
+ // CAN 妯℃嫙鍣�
+ private CanBusSimulator? _canBus;
+ private CanDebugPanel? _canDebugPanel;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ InitializePlugins();
+ InitializeCanSimulator();
+ }
+
+ private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+ {
+ // 娓呯悊 CAN 妯℃嫙鍣�
+ _canBus?.Dispose();
+ CanDriverAdapter.ResetAllBuses();
+
+ // 涓嶅垹闄ゆ祴璇曠洰褰曪紝淇濈暀鏁版嵁浠ヤ究閲嶅惎鍚庢仮澶�
+ // 锛堝闇�鎵嬪姩閲嶇疆锛屼娇鐢ㄥ伐鍏锋爮涓殑 "閲嶇疆鏁版嵁" 鎸夐挳锛�
+ }
+
+ /// <summary>
+ /// 鍒濆鍖栦袱涓彃浠跺苟宓屽叆鍒板搴� Tab 涓�
+ /// </summary>
+ private void InitializePlugins()
+ {
+ // 浣跨敤鍥哄畾鐩綍淇濆瓨鏁版嵁锛堥噸鍚悗涓嶄涪澶憋級
+ string appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+ _testDir = Path.Combine(appData, "PluginIntegrationTest");
+ Directory.CreateDirectory(_testDir);
+ _logFilePath = Path.Combine(_testDir, "test_result.log");
+
+ txtStatus.Text = $"鏁版嵁鐩綍: {_testDir}";
+
+ try
+ {
+ // 鍒濆鍖栦华鍣ㄤ华琛ㄦ敞鍐屾彃浠�
+ _deviceConfigPlugin = new Plugin.DeviceConfig.DeviceConfig();
+ _deviceConfigPlugin.SetBaseFolder(_testDir);
+ _deviceConfigPlugin.SetLogger(_testLog);
+ _deviceConfigPlugin.InitFinish();
+ deviceConfigHost.Child = _deviceConfigPlugin;
+
+ // 鍒濆鍖栫‖浠惰皟璇曟彃浠�
+ _deviceDebugPlugin = new Plugin.DeviceDebug.DeviceDebug();
+ _deviceDebugPlugin.SetBaseFolder(_testDir);
+ _deviceDebugPlugin.SetLogger(_testLog);
+ _deviceDebugPlugin.InitFinish();
+ deviceDebugHost.Child = _deviceDebugPlugin;
+
+ // 棰勭疆 CAN 娴嬭瘯璁惧锛堟柟渚跨洿鎺ラ獙璇侀┍鍔ㄤ笌闈㈡澘锛�
+ SeedPresetCanDevices();
+
+ // 鍒锋柊鎻掍欢鍐呴儴鍒楄〃锛堥渶鎵嬪姩璋冪敤鎻掍欢鏂规硶锛�
+ _deviceConfigPlugin.ForceRefreshForTest();
+ _deviceDebugPlugin.ForceRefreshForTest();
+
+ txtStatus.Text = "鎻掍欢鍒濆鍖栧畬鎴� - 鍙垏鎹� Tab 鎵嬪姩娴嬭瘯";
+ Log("[INFO] 鎻掍欢鍒濆鍖栨垚鍔�");
+ Log($"[INFO] 娴嬭瘯鐩綍: {_testDir}");
+ Log("[INFO] DeviceConfig 宸插祵鍏ュ埌 '浠櫒浠〃娉ㄥ唽' Tab");
+ Log("[INFO] DeviceDebug 宸插祵鍏ュ埌 '纭欢璋冭瘯' Tab");
+ Log("[INFO] 棰勭疆娴嬭瘯璁惧: CAN-001, CAN-002 (Transport=Can)");
+ Log("");
+ Log("[TIP] 浣跨敤娴佺▼:");
+ Log(" 1. 鍏堝垏鎹㈠埌 '浠櫒浠〃娉ㄥ唽' Tab锛屾坊鍔犺澶囥�侀�氶亾銆丼lot鏄犲皠绛�");
+ Log(" 2. 宸查缃� CAN-001 (CAN_BUS_1:0) 鍜� CAN-002 (CAN_BUS_1:1) 渚涙祴璇�");
+ Log(" 3. 鍒囨崲鍒� '纭欢璋冭瘯' Tab锛岄�夋嫨 CAN-001 浼氳嚜鍔ㄦ墦寮� CAN 闈㈡澘骞惰繛鎺ユ�荤嚎");
+ Log(" 4. 鍒囨崲鍒� 'CAN 妯℃嫙鍣�' Tab 鍙墜鍔ㄦ帶鍒舵�荤嚎鎴栧彟涓�涓妭鐐瑰彂閫佹暟鎹�");
+ Log(" 5. 鍙殢鏃剁偣鍑� '杩愯鑷姩鍖栨祴璇�' 楠岃瘉鏍稿績閫昏緫");
+ }
+ catch (Exception ex)
+ {
+ txtStatus.Text = $"鎻掍欢鍒濆鍖栧け璐�: {ex.Message}";
+ Log($"[ERROR] 鎻掍欢鍒濆鍖栧け璐�: {ex.Message}");
+ Log(ex.StackTrace ?? "");
+ }
+ }
+
+ /// <summary>
+ /// 棰勭疆 CAN 娴嬭瘯璁惧锛堜粎褰撲笉瀛樺湪鏃舵彃鍏ワ紝閬垮厤瑕嗙洊鐢ㄦ埛淇敼锛�
+ /// </summary>
+ private void SeedPresetCanDevices()
+ {
+ try
+ {
+ var model = new DeviceConfigModel();
+ string dbPath = Path.Combine(_testDir, "data.db");
+ model.InitDatabase(dbPath);
+
+ int inserted = 0;
+
+ // CAN-001
+ if (model.GetDeviceById("CAN-001") == null)
+ {
+ model.SaveDevice(new DeviceDefinition
+ {
+ DeviceId = "CAN-001",
+ Kind = DeviceKind.CanDevice,
+ DriverType = "CAN",
+ Transport = TransportType.Can,
+ Endpoint = "CAN_BUS_1:0",
+ EffectiveVersion = "V1.0"
+ });
+ inserted++;
+ }
+
+ // CAN-002
+ if (model.GetDeviceById("CAN-002") == null)
+ {
+ model.SaveDevice(new DeviceDefinition
+ {
+ DeviceId = "CAN-002",
+ Kind = DeviceKind.CanDevice,
+ DriverType = "CAN",
+ Transport = TransportType.Can,
+ Endpoint = "CAN_BUS_1:1",
+ EffectiveVersion = "V1.0"
+ });
+ inserted++;
+ }
+
+ Log(inserted > 0
+ ? $"[INFO] 棰勭疆 CAN 璁惧: 鏂板 {inserted} 涓�"
+ : "[INFO] 棰勭疆 CAN 璁惧宸插瓨鍦紝鏈鐩�");
+ }
+ catch (Exception ex)
+ {
+ Log($"[WARN] 棰勭疆 CAN 璁惧澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 閲嶇疆娴嬭瘯鏁版嵁锛堝垹闄ゆ湰鍦扮洰褰曚腑鐨� db 鏂囦欢锛�
+ /// </summary>
+ private void ResetTestData()
+ {
+ try
+ {
+ if (MessageBox.Show("纭畾瑕佸垹闄ゆ湰鍦颁繚瀛樼殑璁惧鏁版嵁鍚楋紵\n姝ゆ搷浣滀笉鍙仮澶嶏紒",
+ "纭閲嶇疆", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes)
+ {
+ return;
+ }
+
+ // 閲婃斁鎻掍欢鍚庡垹闄ゆ暟鎹�
+ _deviceConfigPlugin = null;
+ _deviceDebugPlugin = null;
+
+ foreach (var file in Directory.GetFiles(_testDir, "*.db", SearchOption.AllDirectories))
+ {
+ try { File.Delete(file); } catch { }
+ }
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ MessageBox.Show("鏁版嵁宸查噸缃紝璇烽噸鏂板惎鍔ㄧ▼搴忋��", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"閲嶇疆澶辫触: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void BtnClear_Click(object sender, RoutedEventArgs e)
+ {
+ txtLog.Clear();
+ txtSummary.Text = "灏辩华 - 璇峰垏鎹㈠埌瀵瑰簲 Tab 杩涜鎵嬪姩娴嬭瘯";
+ txtPassed.Text = "";
+ txtFailed.Text = "";
+ progressBar.Value = 0;
+ _passed = 0;
+ _failed = 0;
+ _currentStep = 0;
+ btnRun.IsEnabled = true;
+ }
+
+ private void BtnReset_Click(object sender, RoutedEventArgs e)
+ {
+ ResetTestData();
+ }
+
+ private async void BtnRun_Click(object sender, RoutedEventArgs e)
+ {
+ btnRun.IsEnabled = false;
+ txtLog.Clear();
+ _passed = 0;
+ _failed = 0;
+ _currentStep = 0;
+
+ _totalSteps = CountTestSteps();
+ UpdateProgress();
+
+ Log("========================================");
+ Log(" 浠櫒浠〃娉ㄥ唽 & 纭欢璋冭瘯 闆嗘垚娴嬭瘯");
+ Log($" 鏃堕棿: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
+ Log("========================================");
+ Log("");
+
+ // 涓鸿嚜鍔ㄥ寲娴嬭瘯鍒涘缓鐙珛鐨勬暟鎹簱鐩綍
+ string autoTestDir = Path.Combine(_testDir, "auto_test_" + Guid.NewGuid().ToString("N").Substring(0, 8));
+ Directory.CreateDirectory(autoTestDir);
+
+ await Task.Run(() => RunAllTests(autoTestDir));
+
+ Log("");
+ Log("========================================");
+ string result = _failed == 0 ? "鍏ㄩ儴閫氳繃 鉁�" : $"鏈� {_failed} 椤瑰け璐� 鉁�";
+ Log($" 娴嬭瘯缁撴灉: {result}");
+ Log($" 閫氳繃: {_passed} 澶辫触: {_failed} 鎬昏: {_passed + _failed}");
+ Log("========================================");
+
+ Dispatcher.Invoke(() =>
+ {
+ txtSummary.Text = _failed == 0 ? "鉁� 鑷姩鍖栨祴璇曞叏閮ㄩ�氳繃" : $"鉁� 鏈� {_failed} 椤规祴璇曞け璐�";
+ txtSummary.Foreground = _failed == 0
+ ? System.Windows.Media.Brushes.Green
+ : System.Windows.Media.Brushes.Red;
+ txtPassed.Text = $"閫氳繃: {_passed}";
+ txtFailed.Text = $"澶辫触: {_failed}";
+ btnRun.IsEnabled = true;
+
+ // 澶嶅埗鏃ュ織鍒拌緭鍑虹洰褰�
+ try
+ {
+ string appDir = AppDomain.CurrentDomain.BaseDirectory;
+ string copyPath = Path.Combine(appDir, "test_result.log");
+ File.Copy(_logFilePath, copyPath, overwrite: true);
+ }
+ catch { }
+ });
+ }
+
+ private int CountTestSteps() => 22;
+
+ private void RunAllTests(string testDir)
+ {
+ string configDbPath = Path.Combine(testDir, "device_config.db");
+ string debugDbPath = Path.Combine(testDir, "device_debug.db");
+
+ // ===== 绗竴閮ㄥ垎锛氫华鍣ㄤ华琛ㄦ敞鍐� =====
+ RunSection("绗竴閮ㄥ垎锛氫华鍣ㄤ华琛ㄦ敞鍐� (DeviceConfig)");
+
+ var configModel = TestInit("鍒濆鍖栬澶囬厤缃暟鎹簱", () =>
+ {
+ var model = new DeviceConfigModel();
+ model.InitDatabase(configDbPath);
+ return model;
+ });
+
+ if (configModel == null)
+ {
+ SkipRemainingSection(9, "DeviceConfig 鏁版嵁搴撳垵濮嬪寲澶辫触");
+ }
+ else
+ {
+ var device = TestInit("鍒涘缓璁惧瀹氫箟锛堢數婧愯澶囷級", () =>
+ {
+ var dev = new DeviceDefinition
+ {
+ DeviceId = "PS-001",
+ Kind = DeviceKind.PowerSupply,
+ DriverType = "SCPI",
+ Transport = TransportType.Tcp,
+ Endpoint = "192.168.1.100:5025",
+ EffectiveVersion = "V1.0",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now
+ };
+ configModel.SaveDevice(dev);
+ return dev;
+ });
+
+ TestStep("璇诲彇璁惧瀹氫箟骞堕獙璇�", () =>
+ {
+ var read = configModel.GetDeviceById("PS-001");
+ Assert(read != null, "璁惧搴斿瓨鍦�");
+ Assert(read!.Kind == DeviceKind.PowerSupply, "绉嶇被搴斾负PowerSupply");
+ Assert(read.Transport == TransportType.Tcp, "浼犺緭鏂瑰紡搴斾负Tcp");
+ Assert(read.Endpoint == "192.168.1.100:5025", "绔偣鍦板潃搴斿尮閰�");
+ });
+
+ TestStep("鏇存柊璁惧瀹氫箟", () =>
+ {
+ Assert(device != null, "璁惧瀵硅薄涓嶅簲涓虹┖");
+ device!.Endpoint = "192.168.1.200:5025";
+ device.EffectiveVersion = "V2.0";
+ configModel.SaveDevice(device);
+ var updated = configModel.GetDeviceById("PS-001");
+ Assert(updated!.Endpoint == "192.168.1.200:5025", "绔偣搴斿凡鏇存柊");
+ Assert(updated.EffectiveVersion == "V2.0", "鐗堟湰搴斿凡鏇存柊");
+ });
+
+ TestStep("娣诲姞璁惧閫氶亾", () =>
+ {
+ for (int i = 1; i <= 4; i++)
+ {
+ var ch = new DeviceChannel
+ {
+ DeviceId = "PS-001",
+ ChannelId = i,
+ Capability = i <= 2 ? "Voltage" : "Current",
+ MaxVoltage = 30.0,
+ MaxCurrent = 5.0,
+ Limits = $"{{\"min_v\":0,\"max_v\":30,\"min_i\":0,\"max_i\":5}}",
+ SessionKey = ""
+ };
+ configModel.SaveChannel(ch);
+ }
+ var channels = configModel.GetChannelsByDevice("PS-001");
+ Assert(channels.Count == 4, $"搴旀湁4涓�氶亾锛屽疄闄厈channels.Count}涓�");
+ });
+
+ TestStep("娣诲姞Slot鏄犲皠", () =>
+ {
+ var mapping = new SlotDeviceMapping
+ {
+ SlotId = "SLOT-01",
+ Capability = "Power",
+ Version = "V2.0",
+ DeviceId = "PS-001",
+ ChannelId = 1,
+ MappingVersion = "MV-001"
+ };
+ configModel.SaveSlotMapping(mapping);
+ var mappings = configModel.GetSlotMappings();
+ Assert(mappings.Count == 1, "搴旀湁1鏉lot鏄犲皠");
+ Assert(mappings[0].DeviceId == "PS-001", "鏄犲皠璁惧ID搴斿尮閰�");
+ });
+
+ TestStep("鍒涘缓閰嶇疆鐗堟湰", () =>
+ {
+ var ver = new DeviceConfigVersion
+ {
+ VersionId = "VER-001",
+ Status = VersionStatus.Draft,
+ Hash = configModel.ComputeConfigHash(),
+ Author = "TestUser",
+ Timestamp = DateTime.Now,
+ DiffDescription = "鍒濆閰嶇疆"
+ };
+ configModel.SaveConfigVersion(ver);
+ var versions = configModel.GetConfigVersions();
+ Assert(versions.Count == 1, "搴旀湁1涓増鏈�");
+ Assert(versions[0].Status == VersionStatus.Draft, "鐗堟湰鐘舵�佸簲涓篋raft");
+ Assert(!string.IsNullOrEmpty(versions[0].Hash), "鍝堝笇涓嶅簲涓虹┖");
+ });
+
+ TestStep("鍐茬獊妫�娴嬶紙搴旀棤鍐茬獊锛�", () =>
+ {
+ var conflicts = configModel.DetectConflicts();
+ Assert(conflicts.Count == 0, $"涓嶅簲鏈夊啿绐侊紝瀹為檯: {string.Join("; ", conflicts)}");
+ });
+
+ TestStep("娣诲姞绗簩涓澶囧苟妫�娴婭P鍐茬獊", () =>
+ {
+ var dev2 = new DeviceDefinition
+ {
+ DeviceId = "PS-002",
+ Kind = DeviceKind.PowerSupply,
+ DriverType = "SCPI",
+ Transport = TransportType.Tcp,
+ Endpoint = "192.168.1.200:5025",
+ EffectiveVersion = "V1.0",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now
+ };
+ configModel.SaveDevice(dev2);
+
+ var dev3 = new DeviceDefinition
+ {
+ DeviceId = "PS-003",
+ Kind = DeviceKind.PowerSupply,
+ DriverType = "ModbusTCP",
+ Transport = TransportType.Tcp,
+ Endpoint = "192.168.1.200:5025",
+ EffectiveVersion = "V1.0",
+ CreatedAt = DateTime.Now,
+ UpdatedAt = DateTime.Now
+ };
+ configModel.SaveDevice(dev3);
+
+ var conflicts = configModel.DetectConflicts();
+ Assert(conflicts.Count > 0, "搴旀娴嬪埌IP鍐茬獊");
+ Log($" [鍐茬獊] {conflicts[0]}");
+ });
+
+ TestStep("鍒犻櫎璁惧鍜屾竻鐞�", () =>
+ {
+ configModel.DeleteDevice("PS-003");
+ configModel.DeleteChannel("PS-001", 4);
+ var allDevices = configModel.GetAllDevices();
+ Assert(allDevices.Count == 2, $"鍒犻櫎鍚庡簲鏈�2涓澶囷紝瀹為檯{allDevices.Count}");
+ var channels = configModel.GetChannelsByDevice("PS-001");
+ Assert(channels.Count == 3, $"鍒犻櫎閫氶亾鍚庡簲鏈�3涓紝瀹為檯{channels.Count}");
+ });
+ }
+
+ Log("");
+
+ // ===== 绗簩閮ㄥ垎锛氱‖浠惰皟璇� =====
+ RunSection("绗簩閮ㄥ垎锛氱‖浠惰皟璇� (DeviceDebug)");
+
+ var debugModel = TestInit("鍒濆鍖栬皟璇曟暟鎹簱", () =>
+ {
+ var model = new DeviceDebugModel();
+ model.InitDatabase(debugDbPath);
+ return model;
+ });
+
+ DebugSessionManager? sessionMgr = null;
+ if (debugModel == null)
+ {
+ SkipRemainingSection(8, "DeviceDebug 鏁版嵁搴撳垵濮嬪寲澶辫触");
+ }
+ else
+ {
+ sessionMgr = TestInit("鍒涘缓浼氳瘽绠$悊鍣�", () =>
+ {
+ return new DebugSessionManager(debugModel, resourceManager: null);
+ });
+
+ if (sessionMgr == null)
+ {
+ SkipRemainingSection(7, "浼氳瘽绠$悊鍣ㄥ垱寤哄け璐�");
+ }
+ else
+ {
+ string? sessionId = null;
+ TestStep("鎵撳紑璋冭瘯浼氳瘽锛圧eadOnly锛�", () =>
+ {
+ var session = sessionMgr.OpenSession("PS-001", "OP001", "寮犲伐", "V2.0");
+ sessionId = session.SessionId;
+ Assert(!string.IsNullOrEmpty(sessionId), "浼氳瘽ID涓嶅簲涓虹┖");
+ Assert(session.State == DebugSessionState.ReadOnly, "鍒濆鐘舵�佸簲涓篟eadOnly");
+ Assert(session.DeviceId == "PS-001", "璁惧ID搴斿尮閰�");
+ Log($" 浼氳瘽ID: {sessionId}");
+ });
+
+ if (sessionId == null)
+ {
+ SkipRemainingSection(6, "鎵撳紑浼氳瘽澶辫触");
+ }
+ else
+ {
+ TestStep("璇锋眰缁存姢妯″紡锛圧eadOnly 鈫� AcquiringMaintenance锛�", () =>
+ {
+ bool result = sessionMgr.RequestMaintenance(sessionId!);
+ Assert(result, "璇锋眰缁存姢搴旀垚鍔�");
+ var state = sessionMgr.GetSessionState(sessionId!);
+ Assert(state == DebugSessionState.AcquiringMaintenance, $"鐘舵�佸簲涓篈cquiringMaintenance锛屽疄闄厈state}");
+ });
+
+ TestStep("婵�娲荤淮鎶ゆā寮忥紙AcquiringMaintenance 鈫� MaintenanceActive锛�", () =>
+ {
+ sessionMgr.ActivateMaintenance(sessionId!);
+ var state = sessionMgr.GetSessionState(sessionId!);
+ Assert(state == DebugSessionState.MaintenanceActive, $"鐘舵�佸簲涓篗aintenanceActive锛屽疄闄厈state}");
+ });
+
+ TestStep("楠岃瘉鍛戒护鎵ц鏉冮檺", () =>
+ {
+ var cmd = new PowerSetCommand { Voltage = 12.5, CurrentLimit = 3.0, Output = true };
+ bool allowed = sessionMgr.ValidateCommand(sessionId!, cmd);
+ Assert(allowed, "MaintenanceActive鐘舵�佷笅搴斿厑璁稿懡浠ゆ墽琛�");
+
+ var readOnlySession = sessionMgr.OpenSession("PS-002", "OP002", "鏉庡伐", "V1.0");
+ bool readOnlyAllowed = sessionMgr.ValidateCommand(readOnlySession.SessionId, cmd);
+ Assert(!readOnlyAllowed, "ReadOnly鐘舵�佷笅搴旀嫆缁濆懡浠ゆ墽琛�");
+ });
+
+ TestStep("璁板綍璋冭瘯鎿嶄綔瀹¤", () =>
+ {
+ var op = new DebugOperation
+ {
+ SessionId = sessionId!,
+ CommandType = "PowerSet",
+ Parameters = "{\"voltage\":12.5,\"current\":3.0,\"output\":true}",
+ Outcome = "Success",
+ DeviceReadback = "V=12.48,I=2.99",
+ ErrorCode = "",
+ ErrorMessage = ""
+ };
+ debugModel.RecordOperation(op);
+
+ var ops = debugModel.GetOperationsBySession(sessionId!);
+ Assert(ops.Count == 1, $"搴旀湁1鏉℃搷浣滆褰曪紝瀹為檯{ops.Count}");
+ Assert(ops[0].CommandType == "PowerSet", "鍛戒护绫诲瀷搴斿尮閰�");
+ Assert(ops[0].Outcome == "Success", "缁撴灉搴斾负Success");
+ });
+
+ TestStep("鍏抽棴浼氳瘽锛圡aintenanceActive 鈫� Closing 鈫� Closed锛�", () =>
+ {
+ sessionMgr.CloseSession(sessionId!);
+ var state = sessionMgr.GetSessionState(sessionId!);
+ Assert(state == DebugSessionState.Closed, $"鐘舵�佸簲涓篊losed锛屽疄闄厈state}");
+
+ var session = debugModel.GetSession(sessionId!);
+ Assert(session != null, "浼氳瘽璁板綍搴斾粛瀛樺湪");
+ Assert(session!.ClosedAt.HasValue, "鍏抽棴鏃堕棿搴斿凡璁剧疆");
+ });
+
+ TestStep("鏁呴殰鍜屾仮澶嶆祴璇�", () =>
+ {
+ var faultSession = sessionMgr.OpenSession("PS-001", "OP001", "寮犲伐", "V2.0");
+ string faultId = faultSession.SessionId;
+ sessionMgr.RequestMaintenance(faultId);
+ sessionMgr.ActivateMaintenance(faultId);
+
+ sessionMgr.FaultSession(faultId, "閫氫俊瓒呮椂");
+ var state = sessionMgr.GetSessionState(faultId);
+ Assert(state == DebugSessionState.Faulted, $"鐘舵�佸簲涓篎aulted锛屽疄闄厈state}");
+
+ sessionMgr.MarkRecoveryRequired(faultId);
+ state = sessionMgr.GetSessionState(faultId);
+ Assert(state == DebugSessionState.RecoveryRequired, $"鐘舵�佸簲涓篟ecoveryRequired锛屽疄闄厈state}");
+ });
+ }
+ }
+ }
+
+ Log("");
+
+ // ===== 绗笁閮ㄥ垎锛氳法妯″潡鑱斿姩楠岃瘉 =====
+ RunSection("绗笁閮ㄥ垎锛氳法妯″潡鑱斿姩楠岃瘉");
+
+ if (configModel == null || debugModel == null)
+ {
+ SkipRemainingSection(4, "鍓嶇疆妯″潡鍒濆鍖栧け璐ワ紝璺宠繃鑱斿姩娴嬭瘯");
+ }
+ else
+ {
+ TestStep("璁惧閰嶇疆 鈫� 璋冭瘯浼氳瘽鏁版嵁涓�鑷存��", () =>
+ {
+ var configDevice = configModel.GetDeviceById("PS-001");
+ Assert(configDevice != null, "閰嶇疆涓簲瀛樺湪PS-001");
+
+ var activeSessions = debugModel.GetActiveSessionsByDevice("PS-001");
+ Assert(activeSessions.Count > 0, "PS-001搴旀湁娲昏穬浼氳瘽璁板綍");
+ Assert(activeSessions.All(s => s.DeviceId == "PS-001"), "鎵�鏈変細璇濆簲寮曠敤PS-001");
+ });
+
+ TestStep("閰嶇疆鐗堟湰鍝堝笇涓�鑷存��", () =>
+ {
+ var currentHash = configModel.ComputeConfigHash();
+ var versions = configModel.GetConfigVersions();
+ Assert(versions.Count > 0, "搴旀湁鐗堟湰璁板綍");
+
+ string originalHash = versions[0].Hash;
+ Log($" 鍘熷鍝堝笇: {originalHash.Substring(0, 16)}...");
+ Log($" 褰撳墠鍝堝笇: {currentHash.Substring(0, 16)}...");
+ Log($" 鍝堝笇鍙樺寲: {(originalHash != currentHash ? "鏄紙閰嶇疆宸叉洿鏂帮級" : "鍚�")}");
+ Assert(true, "鍝堝笇璁$畻娴佺▼姝e父");
+ });
+
+ TestStep("鎿嶄綔瀹¤瀹屾暣鎬ч獙璇�", () =>
+ {
+ var allSessions = debugModel.GetAllSessions();
+ int totalOps = 0;
+ foreach (var s in allSessions)
+ {
+ var ops = debugModel.GetOperationsBySession(s.SessionId);
+ totalOps += ops.Count;
+ }
+ Assert(totalOps > 0, $"搴旀湁鎿嶄綔瀹¤璁板綍锛屽疄闄厈totalOps}鏉�");
+ Log($" 鎬讳細璇濇暟: {allSessions.Count}, 鎬绘搷浣滆褰�: {totalOps}");
+ });
+
+ TestStep("璁惧缁勬ā鏉垮姛鑳介獙璇�", () =>
+ {
+ var template = new DeviceGroupTemplate
+ {
+ TemplateId = "TPL-001",
+ DeviceType = "PowerSupply",
+ DriverType = "SCPI",
+ Transport = "Tcp",
+ DeviceCount = 4,
+ ChannelsPerDevice = 4,
+ AddressRule = "192.168.1.{0}",
+ SlotStart = 1
+ };
+ configModel.SaveGroupTemplate(template);
+ var templates = configModel.GetGroupTemplates();
+ Assert(templates.Count == 1, "搴旀湁1涓ā鏉�");
+ Assert(templates[0].DeviceCount == 4, "璁惧鏁伴噺搴斾负4");
+ });
+ }
+ }
+
+ #region CAN 妯℃嫙鍣�
+
+ /// <summary>
+ /// 鍒濆鍖� CAN 妯℃嫙鍣�
+ /// </summary>
+ private void InitializeCanSimulator()
+ {
+ try
+ {
+ // 鍒涘缓 CAN 璋冭瘯闈㈡澘骞跺祵鍏ュ埌 Tab 涓�
+ _canDebugPanel = new CanDebugPanel();
+ canSimulatorHost.Child = _canDebugPanel;
+
+ Log("[INFO] CAN 妯℃嫙鍣ㄥ凡宓屽叆鍒� 'CAN 妯℃嫙鍣�' Tab");
+ }
+ catch (Exception ex)
+ {
+ Log($"[ERROR] CAN 妯℃嫙鍣ㄥ垵濮嬪寲澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 鍚姩鎬荤嚎鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void CanStartBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ string busName = canBusNameInput.Text.Trim();
+ if (string.IsNullOrEmpty(busName))
+ {
+ MessageBox.Show("璇疯緭鍏ユ�荤嚎鍚嶇О", "鎻愮ず", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // 閫氳繃 DriverFactory 鍒涘缓 CAN 椹卞姩鏉ュ惎鍔ㄦ�荤嚎
+ var driver = DriverFactory.CreateDriver("CAN_SIM_0", "CAN", $"{busName}:0");
+ driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult();
+
+ // 鑾峰彇鎬荤嚎瀹炰緥
+ var buses = CanDriverAdapter.GetAllBuses();
+ if (buses.TryGetValue(busName, out var bus))
+ {
+ _canBus = bus;
+ bus.Start();
+
+ // 鏇存柊 UI
+ canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
+ canBusStatusText.Text = $"杩愯涓� ({busName})";
+ canStartBtn.IsEnabled = false;
+ canStopBtn.IsEnabled = true;
+
+ Log($"[CAN] 鎬荤嚎 {busName} 宸插惎鍔�");
+ }
+ }
+ catch (Exception ex)
+ {
+ Log($"[CAN] 鍚姩鎬荤嚎澶辫触: {ex.Message}");
+ MessageBox.Show($"鍚姩澶辫触: {ex.Message}", "閿欒", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ /// <summary>
+ /// 鍋滄鎬荤嚎鎸夐挳鐐瑰嚮
+ /// </summary>
+ private void CanStopBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ _canBus?.Stop();
+
+ // 鏇存柊 UI
+ canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E"));
+ canBusStatusText.Text = "宸插仠姝�";
+ canStartBtn.IsEnabled = true;
+ canStopBtn.IsEnabled = false;
+
+ Log("[CAN] 鎬荤嚎宸插仠姝�");
+ }
+ catch (Exception ex)
+ {
+ Log($"[CAN] 鍋滄鎬荤嚎澶辫触: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// 閲嶇疆鎵�鏈夋寜閽偣鍑�
+ /// </summary>
+ private void CanResetBtn_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ CanDriverAdapter.ResetAllBuses();
+ _canBus = null;
+
+ // 鏇存柊 UI
+ canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E"));
+ canBusStatusText.Text = "鏈惎鍔�";
+ canStartBtn.IsEnabled = true;
+ canStopBtn.IsEnabled = false;
+
+ Log("[CAN] 鎵�鏈夋�荤嚎宸查噸缃�");
+ }
+ catch (Exception ex)
+ {
+ Log($"[CAN] 閲嶇疆澶辫触: {ex.Message}");
+ }
+ }
+
+ #endregion
+
+ #region 娴嬭瘯杈呭姪鏂规硶
+
+ private T? TestInit<T>(string name, Func<T> action) where T : class
+ {
+ _currentStep++;
+ UpdateProgress();
+ try
+ {
+ var result = action();
+ _passed++;
+ LogStep("PASS", name);
+ return result;
+ }
+ catch (Exception ex)
+ {
+ _failed++;
+ LogStep("FAIL", name, ex.Message);
+ return default;
+ }
+ }
+
+ private void TestStep(string name, Action action)
+ {
+ _currentStep++;
+ UpdateProgress();
+ try
+ {
+ action();
+ _passed++;
+ LogStep("PASS", name);
+ }
+ catch (Exception ex)
+ {
+ _failed++;
+ LogStep("FAIL", name, ex.Message);
+ }
+ }
+
+ private void Assert(bool condition, string message)
+ {
+ if (!condition)
+ throw new Exception($"鏂█澶辫触: {message}");
+ }
+
+ private void RunSection(string title)
+ {
+ Log($"--- {title} ---");
+ Log("");
+ }
+
+ private void SkipRemainingSection(int count, string reason)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ _currentStep++;
+ _failed++;
+ UpdateProgress();
+ LogStep("FAIL", $"[SKIP] 渚濊禆澶辫触: {reason}");
+ }
+ }
+
+ private void LogStep(string status, string name, string? error = null)
+ {
+ string line = status == "PASS"
+ ? $" [PASS] {name}"
+ : $" [FAIL] {name} 鈫� {error}";
+ Log(line);
+ }
+
+ private void Log(string message)
+ {
+ _logBuffer.AppendLine(message);
+ Dispatcher.Invoke(() =>
+ {
+ txtLog.AppendText(message + Environment.NewLine);
+ txtLog.ScrollToEnd();
+ });
+ if (!string.IsNullOrEmpty(_logFilePath))
+ {
+ try { File.AppendAllText(_logFilePath, message + Environment.NewLine); }
+ catch { }
+ }
+ }
+
+ private void UpdateProgress()
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (_totalSteps > 0)
+ {
+ double pct = (_currentStep * 100.0) / _totalSteps;
+ progressBar.Value = Math.Min(pct, 100);
+ }
+ });
+ }
+
+ #endregion
+ }
+}
diff --git a/Plugin.IntegrationTest/Plugin.IntegrationTest.csproj b/Plugin.IntegrationTest/Plugin.IntegrationTest.csproj
new file mode 100644
index 0000000..4377921
--- /dev/null
+++ b/Plugin.IntegrationTest/Plugin.IntegrationTest.csproj
@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>WinExe</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects>
+ <PlatformTarget>x64</PlatformTarget>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="log4net" Version="3.0.4" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Plugin.DeviceConfig\Plugin.DeviceConfig.csproj" />
+ <ProjectReference Include="..\Plugin.DeviceDebug\Plugin.DeviceDebug.csproj" />
+ <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" />
+ <ProjectReference Include="..\Aging.Infrastructure\Aging.Infrastructure.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Content Include="..\Libs\SQLite.Interop.dll" Link="SQLite.Interop.dll">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ <Content Include="..\Libs\SQLite.Interop.dll" Link="x64\SQLite.Interop.dll">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
+
+</Project>
diff --git a/Plugin.StandardTest/AssemblyInfo.cs b/Plugin.StandardTest/AssemblyInfo.cs
new file mode 100644
index 0000000..1a34d01
--- /dev/null
+++ b/Plugin.StandardTest/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+using HMI.Core.Annotation;
+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)
+)]
+[assembly: PluginClassName("Plugin.StandardTest.StandardTestUI")]
diff --git a/Plugin.StandardTest/Entity/TestStep.cs b/Plugin.StandardTest/Entity/TestStep.cs
new file mode 100644
index 0000000..4887b36
--- /dev/null
+++ b/Plugin.StandardTest/Entity/TestStep.cs
@@ -0,0 +1,14 @@
+锘縩amespace Plugin.StandardTest.Entity
+{
+ public class TestStep
+ {
+ public string Name { get; set; }
+ public string Step { get; set; }
+ public string Value { get; set; }
+ public string LimitUp { get; set; }
+ public string LimitDown { get; set; }
+ public string Unit { get; set; }
+ public string ConsumeTime { get; set; }
+ public string Result { get; set; }
+ }
+}
diff --git a/Plugin.StandardTest/Plugin.StandardTest.csproj b/Plugin.StandardTest/Plugin.StandardTest.csproj
new file mode 100644
index 0000000..21036e2
--- /dev/null
+++ b/Plugin.StandardTest/Plugin.StandardTest.csproj
@@ -0,0 +1,63 @@
+锘�<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ <Platforms>AnyCPU;x64</Platforms>
+ <AssemblyVersion>1.0.0.0</AssemblyVersion>
+ <FileVersion>1.0.0.0</FileVersion>
+ <IncludeSymbols>False</IncludeSymbols>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ <PackageReference Include="LiveChartsCore" Version="2.0.0-rc5.4" />
+ <PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.0.0-rc5.4" />
+ <PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc5.4" />
+ <PackageReference Include="log4net" Version="3.0.4" />
+ <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+ <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
+ <PackageReference Include="SuperSocket" Version="1.6.6.1" />
+ <PackageReference Include="SuperSocket.Engine" Version="1.6.6.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" />
+ <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
+ <ProjectReference Include="..\HMI.StdControl\HMI.StdControl.csproj" />
+ <ProjectReference Include="..\PLCHelper\PLCHelper.csproj" />
+ <ProjectReference Include="..\Plugin.TsSupport\Plugin.TsSupport.csproj" />
+ <ProjectReference Include="..\SimpleUtil\HMI.LogUtil\HMI.LogUtil.csproj" />
+ <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="HslCommunication">
+ <HintPath>..\Libs\HslCommunication.dll</HintPath>
+ </Reference>
+ <Reference Include="NationalInstruments.TestStand.Interop.API">
+ <HintPath>..\Libs\NationalInstruments.TestStand.Interop.API.dll</HintPath>
+ </Reference>
+ <Reference Include="NationalInstruments.TestStand.Interop.UI.AxControls">
+ <HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath>
+ </Reference>
+ <Reference Include="NPOI">
+ <HintPath>..\HMI.ParameterManage\bin\x64\Debug\NPOI.dll</HintPath>
+ </Reference>
+ <Reference Include="System.Net.Http" />
+ <Reference Include="System.Windows.Forms" />
+ <Reference Include="WindowsFormsIntegration" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Compile Update="StandardTestUI.xaml.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ </ItemGroup>
+
+</Project>
diff --git a/Plugin.StandardTest/Scanner.xaml b/Plugin.StandardTest/Scanner.xaml
new file mode 100644
index 0000000..505dd0f
--- /dev/null
+++ b/Plugin.StandardTest/Scanner.xaml
@@ -0,0 +1,77 @@
+锘�<pu:WindowX x:Class="Plugin.StandardTest.Scanner"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ WindowStartupLocation="CenterScreen"
+ Closing="Window_Closing"
+ FontSize="20"
+ mc:Ignorable="d"
+ Style="{StaticResource ModalWindow}"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:cc="clr-namespace:HMI.Core;assembly=HMI.Core"
+ InteropOwnersMask="False"
+ SizeToContent="Height"
+ pu:WindowXModalDialog.Buttons="{x:Null}"
+ Title="{Binding title}" Width="600">
+ <Window.Resources>
+ <Style TargetType="Label">
+ <Setter Property="HorizontalAlignment" Value="Right"/>
+ <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window},
+Converter={StaticResource fc}, ConverterParameter='80,1920'}" />
+ </Style>
+ <Style TargetType="TextBox">
+ <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window},
+Converter={StaticResource fc}, ConverterParameter='100,1920'}" />
+ </Style>
+ <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
+ <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window},
+Converter={StaticResource fc}, ConverterParameter='20,1920'}" />
+ </Style>
+ </Window.Resources>
+
+ <Grid Margin="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition/>
+ <ColumnDefinition/>
+ </Grid.ColumnDefinitions>
+
+ <!--<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" Padding="0"
+ Content="{DynamicResource Tr.PleaseScanSnr}"
+ Margin="0,0,0,20"
+ />-->
+ <GroupBox Grid.Row="1" Header="{DynamicResource Tr.PleaseScanSnr}" Padding="0,0,0,10" Grid.ColumnSpan="2" >
+ <StackPanel>
+ <pu:FormGroup
+ Header="{DynamicResource Tr.PositionSnr}"
+ dp:FormGroupHelper.IsRequired="True"
+ Style="{StaticResource InputFormGroupStyle}">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="positionText" TextWrapping="NoWrap"/>
+ </pu:FormGroup>
+
+ <pu:FormGroup
+ Header="{DynamicResource Tr.ProductSnr}"
+ dp:FormGroupHelper.IsRequired="True" Style="{StaticResource InputFormGroupStyle}">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="snrText" TextWrapping="NoWrap"/>
+ </pu:FormGroup>
+ </StackPanel>
+
+ </GroupBox>
+
+ <Button Grid.Row="2" Click="Con_Click" Style="{StaticResource PrimaryButton}"
+ Content="{DynamicResource Tr.ContinueScan}"/>
+ <Button Grid.Row="2" Grid.Column="1" Click="Ok_Click" Style="{StaticResource SuccessButton}"
+ Content="{DynamicResource Tr.FinishScan}"/>
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.StandardTest/Scanner.xaml.cs b/Plugin.StandardTest/Scanner.xaml.cs
new file mode 100644
index 0000000..b268a0b
--- /dev/null
+++ b/Plugin.StandardTest/Scanner.xaml.cs
@@ -0,0 +1,209 @@
+锘縰sing HMI.Core.Util;
+using HMI.UI.Entity;
+using HMI.UI.Util;
+using HMI.Util;
+using Panuon.WPF.UI;
+using System;
+using System.ComponentModel;
+using System.IO.Ports;
+using System.Windows;
+
+namespace Plugin.StandardTest
+{
+ public class BarcodeEventArg
+ {
+ public string name { get; private set; }
+ public string positionCode { get; private set; }
+ public string snrCode { get; private set; }
+
+ public BarcodeEventArg(string name, string positionCode, string snrCode)
+ {
+ this.name = name;
+ this.positionCode = positionCode;
+ this.snrCode = snrCode;
+ }
+ }
+
+ /// <summary>
+ /// Scanner.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class Scanner : WindowX
+ {
+ private string _title;
+
+ public string title
+ {
+ get { return _title; }
+ set => Set(ref _title, value);
+ }
+
+ public event EventHandler<BarcodeEventArg> FinishBarcode;
+ SerialPort serialPort;
+ public Scanner(string title)
+ {
+ this.title = title;
+ this.DataContext = this;
+ InitializeComponent();
+ Loaded += Scanner_Loaded;
+ }
+
+ private void Scanner_Loaded(object sender, RoutedEventArgs e)
+ {
+ InitScanner();
+ Loaded -= Scanner_Loaded;
+ }
+
+ /// <summary>
+ /// 璁剧疆鎵爜鏋�
+ /// </summary>
+ private void InitScanner()
+ {
+ string comStr = "";
+ if (serialPort != null) return;
+
+ string section = this.Tag.ToString();
+ ScannerConfig common = ScannerConfig.ReadFromIni("", "Config");
+
+ string portName = IniFileHelper.ReadIniData(section, "PortName", "","");
+ if (string.IsNullOrEmpty(portName))
+ {
+ if (section.StartsWith("A"))
+ {
+ portName = "COM1";
+ }
+ else
+ {
+ portName = "COM2";
+ }
+ }
+ common.PortName = portName;
+
+
+ // 閰嶇疆涓插彛
+ serialPort = new SerialPort
+ {
+ PortName = common.PortName,
+ BaudRate = common.BaudRate,
+ Parity = Parity.None,
+ DataBits = common.DataBits,
+ StopBits = common.StopBits,
+ Handshake = Handshake.None,
+ DtrEnable = common.DtrEnable,
+ RtsEnable = common.RtsEnable
+ //ReadTimeout = 500,
+ //WriteTimeout = 500,
+ //ReciveBufferSize=4096
+ };
+
+ try
+ {
+ // 鎵撳紑涓插彛
+ serialPort.Open();
+ // 娉ㄥ唽鎺ユ敹鏁版嵁浜嬩欢
+ serialPort.DataReceived += SerialPort_DataReceived;
+ }
+ catch (Exception ex)
+ {
+ MsgBoxHelper.Error(this, $"{this.lang("Tr.InitGunFailed")}: {ex.StackTrace}");
+ }
+ }
+
+ private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
+ {
+ try
+ {
+ SerialPort sp = (SerialPort)sender;
+ string barcode = ReadDataFrom(sp);
+
+ //string barcode =
+ Dispatcher.Invoke(() =>
+ {
+ if (snrText.IsFocused)
+ {
+ snrText.Text = barcode;
+ Con_Click(null, null);
+ }
+ else
+ {
+ positionText.Text = barcode;
+ //positionText.Focus();
+ //snrText.Text = "";
+ snrText.Focus();
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"{this.lang("Tr.GetGunData")}: {ex.StackTrace}");
+ }
+ }
+
+ private void Con_Click(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ string position = positionText.Text, snr = snrText.Text;
+ if (string.IsNullOrEmpty(position) || string.IsNullOrEmpty(snr))
+ {
+ ToastHelper.Warning(this, this.lang("Tr.PleaseInputSnrs"));
+ return;
+ }
+ try
+ {
+ FinishBarcode.Invoke(this, new BarcodeEventArg(Tag.ToString(), position, snr));
+ //positionText.Text = "";
+ //snrText.Text = "";
+ positionText.Focus();
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Warning(this, ex.Message);
+ }
+ });
+ }
+
+ private void Ok_Click(object sender, RoutedEventArgs e)
+ {
+ string position = positionText.Text, snr = snrText.Text;
+ if (string.IsNullOrEmpty(position) || string.IsNullOrEmpty(snr))
+ {
+ ToastHelper.Warning(this, this.lang("Tr.PleaseInputSnrs"));
+ return;
+ }
+ try
+ {
+ FinishBarcode.Invoke(this, new BarcodeEventArg(Tag.ToString(), position, snr));
+ this.Close();
+ }
+ catch (Exception ex)
+ {
+ ToastHelper.Warning(this, ex.Message);
+ }
+ }
+
+ private string ReadDataFrom(SerialPort sp)
+ {
+ string barcode = "";
+ while (sp.BytesToRead > 0)
+ {
+ byte[] buffer = new byte[sp.BytesToRead];
+ sp.Read(buffer, 0, buffer.Length);
+ barcode += System.Text.Encoding.UTF8.GetString(buffer);
+ System.Threading.Thread.Sleep(50);
+ }
+ return barcode;
+ }
+
+ private void Window_Closing(object sender, CancelEventArgs e)
+ {
+ try
+ {
+ serialPort.Close();
+ serialPort = null;
+ }
+ catch (Exception ex)
+ {
+ }
+ }
+ }
+}
diff --git a/Plugin.StandardTest/ScannerManager.cs b/Plugin.StandardTest/ScannerManager.cs
new file mode 100644
index 0000000..c8994fd
--- /dev/null
+++ b/Plugin.StandardTest/ScannerManager.cs
@@ -0,0 +1,99 @@
+锘縰sing HMI.UI.Entity;
+using System.IO.Ports;
+using System.Windows.Controls;
+
+namespace Plugin.StandardTest
+{
+ public class ScannerManager
+ {
+ public static Dictionary<string, SerialPort> map = new Dictionary<string, SerialPort>();
+ public static Dictionary<SerialPort, Queue<TextBox>> controlMap = new Dictionary<SerialPort, Queue<TextBox>>();
+
+ public static Dictionary<SerialPort, TextBox> focusMap = new Dictionary<SerialPort, TextBox>();
+ public static Dictionary<TextBox, SerialPort> reverseMap = new Dictionary<TextBox, SerialPort>();
+
+
+ public static void RegisterCom(ScannerConfig setting, TextBox tb)
+ {
+ if (!map.ContainsKey(setting.PortName))
+ {
+ // 閰嶇疆涓插彛
+ var serialPort = new SerialPort
+ {
+ PortName = setting.PortName,
+ BaudRate = setting.BaudRate,
+ Parity = Parity.None,
+ DataBits = setting.DataBits,
+ StopBits = setting.StopBits,
+ Handshake = Handshake.None,
+ DtrEnable = setting.DtrEnable,
+ RtsEnable = setting.RtsEnable
+ //ReadTimeout = 500,
+ //WriteTimeout = 500,
+ //ReciveBufferSize=4096
+ };
+
+ try
+ {
+ // 鎵撳紑涓插彛
+ serialPort.Open();
+ map[setting.PortName] = serialPort;
+ controlMap[serialPort] = new Queue<TextBox>();
+ reverseMap[tb] = serialPort;
+ tb.GotFocus += Tb_GotFocus;
+ controlMap[serialPort].Enqueue(tb);
+
+ // 娉ㄥ唽鎺ユ敹鏁版嵁浜嬩欢
+ serialPort.DataReceived += SerialPort_DataReceived; ;
+ }
+ catch (Exception ex)
+ {
+
+ }
+ }
+ else {
+
+ }
+ }
+
+ private static string ReadDataFrom(SerialPort sp)
+ {
+ string barcode = "";
+ while (sp.BytesToRead > 0)
+ {
+ byte[] buffer = new byte[sp.BytesToRead];
+ sp.Read(buffer, 0, buffer.Length);
+ barcode += System.Text.Encoding.UTF8.GetString(buffer);
+ System.Threading.Thread.Sleep(50);
+ }
+ return barcode;
+ }
+
+ private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
+ {
+ var sp = sender as SerialPort;
+ string barcode = ReadDataFrom(sp);
+ TextBox tb;
+ if (focusMap.ContainsKey(sp))
+ {
+ tb = focusMap[sp];
+ }
+ else
+ {
+ tb = controlMap[sp].Dequeue();
+ }
+ //string barcode =
+ tb.Dispatcher.Invoke(() =>
+ {
+ tb.Text = barcode;
+ });
+ }
+
+ private static void Tb_GotFocus(object sender, System.Windows.RoutedEventArgs e)
+ {
+ var tb = sender as TextBox;
+ SerialPort sp = reverseMap[tb];
+ focusMap[sp] = tb;
+ }
+ }
+}
diff --git a/Plugin.StandardTest/ScreenGrid.xaml b/Plugin.StandardTest/ScreenGrid.xaml
new file mode 100644
index 0000000..df94da6
--- /dev/null
+++ b/Plugin.StandardTest/ScreenGrid.xaml
@@ -0,0 +1,15 @@
+锘�<hb:NotifyControl x:Class="Plugin.StandardTest.ScreenGrid"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ mc:Ignorable="d"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ Background="LightGray" Padding="0" Margin="0"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <Grid x:Name="grid" dp:GridHelper.Space="2" Margin="0">
+
+ </Grid>
+</hb:NotifyControl>
diff --git a/Plugin.StandardTest/ScreenGrid.xaml.cs b/Plugin.StandardTest/ScreenGrid.xaml.cs
new file mode 100644
index 0000000..c524214
--- /dev/null
+++ b/Plugin.StandardTest/ScreenGrid.xaml.cs
@@ -0,0 +1,45 @@
+锘縰sing HMI.Base;
+using HMI.UI.DpUtil;
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Plugin.StandardTest
+{
+ public class ScreenGridConfig
+ {
+ public int rows { get; set; }
+ public int columns { get; set; }
+ public ObservableCollection<bool> values { get; set; }
+ }
+
+ /// <summary>
+ /// ScreenTouch.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class ScreenGrid : NotifyControl
+ {
+ public ScreenGrid(int rows, int columns)
+ {
+ InitializeComponent();
+
+ for (int i = 0; i < rows; i++)
+ {
+ grid.RowDefinitions.Add(new RowDefinition());
+ }
+
+ for (int i = 0; i < columns; i++)
+ {
+ grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto});
+ }
+ }
+
+ public void AddScreen(ScreenTouch screen, int row, int col)
+ {
+ grid.Children.Add(screen);
+ Grid.SetRow(screen, row);
+ Grid.SetColumn(screen, col);
+
+ GridHelper.RefreshGridSpace(grid);
+ }
+ }
+}
diff --git a/Plugin.StandardTest/ScreenTouch.xaml b/Plugin.StandardTest/ScreenTouch.xaml
new file mode 100644
index 0000000..c3c06a7
--- /dev/null
+++ b/Plugin.StandardTest/ScreenTouch.xaml
@@ -0,0 +1,14 @@
+锘�<hb:NotifyControl x:Class="Plugin.StandardTest.ScreenTouch"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ mc:Ignorable="d"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UniformGrid x:Name="uniform" dp:UniformGridHelper.InnerBorder="0.5"
+ dp:UniformGridHelper.InnerBorderBrush="#37375f">
+ </UniformGrid>
+</hb:NotifyControl>
diff --git a/Plugin.StandardTest/ScreenTouch.xaml.cs b/Plugin.StandardTest/ScreenTouch.xaml.cs
new file mode 100644
index 0000000..698b646
--- /dev/null
+++ b/Plugin.StandardTest/ScreenTouch.xaml.cs
@@ -0,0 +1,179 @@
+锘縰sing HMI.Base;
+using HMI.UI.DpUtil;
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+
+namespace Plugin.StandardTest
+{
+ public class ColorConverter : IMultiValueConverter
+ {
+ public static ColorConverter Instance = new ColorConverter();
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values[0] is ObservableCollection<string> list && values[1] is string tag)
+ {
+ if (list?.Contains(tag) == true) return Brushes.Green;
+ return Brushes.White;
+ }
+ return values;
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// ScreenTouch.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class ScreenTouch : NotifyControl
+ {
+ public static readonly DependencyProperty ValuesProperty = DependencyProperty.Register(
+ nameof(Values),
+ typeof(ObservableCollection<bool>),
+ typeof(ScreenTouch),
+ new PropertyMetadata(null, OnValuesChanged));
+
+ private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ScreenTouch control)
+ {
+ // 濡傛灉鏈夋棫鐨勯泦鍚堬紝鍙栨秷璁㈤槄
+ if (e.OldValue is ObservableCollection<bool> oldCollection)
+ {
+ oldCollection.CollectionChanged -= control.Value_CollectionChanged;
+ }
+
+ // 濡傛灉鏈夋柊鐨勯泦鍚堬紝璁㈤槄鍙樻洿浜嬩欢
+ if (e.NewValue is ObservableCollection<bool> newCollection)
+ {
+ newCollection.CollectionChanged += control.Value_CollectionChanged;
+ }
+
+ // 鍒锋柊鎵�鏈夊崟鍏冩牸
+ control.PaintPoints();
+ }
+ }
+
+
+ public ScreenTouch(int rows, int columns)
+ {
+ this.rows = rows;
+ this.columns = columns;
+ InitializeComponent();
+ Refresh();
+ Values = new ObservableCollection<bool>(new bool[rows * columns]);
+ }
+
+ public ObservableCollection<bool> Values
+ {
+ get => (ObservableCollection<bool>)GetValue(ValuesProperty);
+ set
+ {
+ SetValue(ValuesProperty, value);
+ }
+ }
+
+ private void Value_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+ {
+ PaintPoints();
+ }
+
+ public int rows { get; private set; }
+ public int columns { get; private set; }
+
+ private Border[] children;
+
+ public void Refresh()
+ {
+ uniform.Children.Clear();
+ uniform.Rows = rows;
+ uniform.Columns = columns;
+
+ children = new Border[rows * columns];
+
+
+ for (int i = 0; i < children.Length; i++)
+ {
+ var border = new Border
+ {
+ BorderThickness = new Thickness(0),
+ Background = Brushes.White
+ };
+ children[i] = border;
+ uniform.Children.Add(border);
+ }
+ UniformGridHelper.RefreshGridInnerBorder(uniform);
+ }
+
+ public void Refresh(int rows, int columns)
+ {
+ this.rows = rows;
+ this.columns = columns;
+ Refresh();
+ }
+
+ public void Clear()
+ {
+ Values = new ObservableCollection<bool>(new bool[rows * columns]);
+ }
+
+ public void DrawPoint(int x, int y)
+ {
+ Values[y * columns + x] = true;
+ }
+
+ public void DrawPoints(int[][] points)
+ {
+ var temp = new ObservableCollection<bool>(new bool[rows * columns]);
+ for (int i = 0; i < points.Length; i++)
+ {
+ int[] point = points[i];
+ int index = point[1] * columns + point[0];
+ //children[point[0]][point[1]].Background = Brushes.Green;
+ temp[index] = true;
+ }
+ Values = temp;
+ }
+
+ public void DrawNewPoints(int[][] points)
+ {
+ var temp = Values == null ?
+ new ObservableCollection<bool>(new bool[rows * columns]) :
+ new ObservableCollection<bool>(Values);
+ for (int i = 0; i < points.Length; i++)
+ {
+ int[] point = points[i];
+ if (point == null)
+ {
+ continue;
+ }
+ int index = point[1] * columns + point[0];
+ //children[point[0]][point[1]].Background = Brushes.Green;
+ temp[index] = true;
+ }
+ Values = temp;
+ }
+
+ private void PaintPoints()
+ {
+ for (int i = 0; i < Values.Count; i++)
+ {
+ if (Values[i])
+ {
+ children[i].Background = Brushes.Green;
+ }
+ else
+ {
+ children[i].Background = Brushes.White;
+ }
+ }
+ }
+ }
+}
diff --git a/Plugin.StandardTest/ScreenTouchWin.xaml b/Plugin.StandardTest/ScreenTouchWin.xaml
new file mode 100644
index 0000000..ccc1b3f
--- /dev/null
+++ b/Plugin.StandardTest/ScreenTouchWin.xaml
@@ -0,0 +1,20 @@
+锘�<pu:WindowX x:Class="Plugin.StandardTest.ScreenTouchWin"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ mc:Ignorable="d"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ SizeChanged="Window_SizeChanged"
+ Style="{StaticResource ModalWindow}"
+ pu:WindowXModalDialog.Buttons="{x:Null}"
+ WindowStartupLocation="CenterScreen"
+ WindowState="Maximized"
+ Background="LightGray"
+ Title="Screens" Height="450" Width="800">
+ <Grid x:Name="grid" HorizontalAlignment="Center">
+
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.StandardTest/ScreenTouchWin.xaml.cs b/Plugin.StandardTest/ScreenTouchWin.xaml.cs
new file mode 100644
index 0000000..ff5d892
--- /dev/null
+++ b/Plugin.StandardTest/ScreenTouchWin.xaml.cs
@@ -0,0 +1,67 @@
+锘縰sing Panuon.WPF.UI;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Shapes;
+
+namespace Plugin.StandardTest
+{
+ /// <summary>
+ /// ScreenTouchWin.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class ScreenTouchWin : WindowX
+ {
+ private ScreenGrid screenGrid;
+ private ScreenTouch[] screens;
+
+ public ScreenTouchWin(ScreenTouch[] configs)
+ {
+ screenGrid = new ScreenGrid(1, configs.Length);
+ screens = new ScreenTouch[configs.Length];
+ InitializeComponent();
+
+ int space = 2 * (screens.Length + 1);
+ for (int i = 0; i < screens.Length; i++)
+ {
+ var screen = new ScreenTouch(configs[i].rows, configs[i].columns)
+ {
+ Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0),
+ Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0)
+ };
+
+ screen.SetBinding(ScreenTouch.ValuesProperty, new Binding(nameof(ScreenTouch.Values)) { Source = configs[i], Mode = BindingMode.OneWay });
+
+ screens[i] = screen;
+ screenGrid.AddScreen(screen, 0, i);
+ }
+
+ grid.Children.Add(screenGrid);
+ }
+
+ private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (screenGrid == null)
+ {
+ return;
+ }
+ int space = 2 * (screens.Length + 1);
+
+ for (int i = 0; i < screens.Length; i++)
+ {
+ var screen = screens[i];
+ screen.Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0);
+ screen.Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0);
+ }
+ }
+ }
+}
diff --git a/Plugin.StandardTest/Setting/TcpServer.cs b/Plugin.StandardTest/Setting/TcpServer.cs
new file mode 100644
index 0000000..25d5685
--- /dev/null
+++ b/Plugin.StandardTest/Setting/TcpServer.cs
@@ -0,0 +1,49 @@
+锘縰sing SuperSocket.SocketBase;
+using SuperSocket.SocketBase.Protocol;
+using System.Text;
+
+namespace Plugin.StandardTest.Setting
+{
+ public class DTReceiveFilter : ReceiveFilterBase<StringRequestInfo>
+ {
+ public override StringRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest)
+ {
+ rest = 0;
+ int i = offset;
+ List<byte> bytes = new List<byte>();
+ while (i < readBuffer.Length)
+ {
+ byte b = readBuffer[i++];
+ if (b == '\n')
+ {
+ break;
+ }
+ bytes.Add(b);
+ }
+ byte[] arr = bytes.ToArray();
+ return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), Encoding.UTF8.GetString(arr), null);//sb.ToString()
+ }
+ }
+
+ public class StringParser : IRequestInfoParser<StringRequestInfo>
+ {
+
+ public StringRequestInfo ParseRequestInfo(string source)
+ {
+ return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), source, null);
+ }
+ }
+
+ public class TcpServer : AppServer<TcpSession>
+ {
+ public TcpServer() : base(new TerminatorReceiveFilterFactory("\n", Encoding.UTF8, new StringParser()))
+ {
+
+ }
+ }
+
+ public class TcpSession : AppSession<TcpSession, StringRequestInfo>
+ {
+
+ }
+}
diff --git a/Plugin.StandardTest/Setting/TsSetting.cs b/Plugin.StandardTest/Setting/TsSetting.cs
new file mode 100644
index 0000000..dd65111
--- /dev/null
+++ b/Plugin.StandardTest/Setting/TsSetting.cs
@@ -0,0 +1,6 @@
+锘縩amespace Plugin.StandardTest.Setting
+{
+ public class TsSetting
+ {
+ }
+}
diff --git a/Plugin.StandardTest/SingleControl.xaml b/Plugin.StandardTest/SingleControl.xaml
new file mode 100644
index 0000000..f268b06
--- /dev/null
+++ b/Plugin.StandardTest/SingleControl.xaml
@@ -0,0 +1,419 @@
+锘�<UserControl x:Class="Plugin.StandardTest.SingleControl"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ xmlns:cdp="clr-namespace:HMI.Core.DpUtil;assembly=HMI.Core"
+ mc:Ignorable="d"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib"
+ Loaded="UserControl_Loaded"
+ Background="#ccc6c7"
+ PreviewMouseRightButtonDown="control_PreviewMouseRightButtonDown"
+ FontSize="{Binding ActualHeight,RelativeSource={RelativeSource AncestorType=UserControl},
+Converter={StaticResource hfc}, ConverterParameter='12,816'}"
+ x:Name="control"
+ SizeChanged="control_SizeChanged"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/>
+ <ResourceDictionary Source="/HMI.UI;component/Style/PanuonDataGrid.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ <sys:Double x:Key="nanD">NaN</sys:Double>
+
+ <Style TargetType="Label" x:Key="baseLabel">
+ <Setter Property="HorizontalContentAlignment" Value="Left"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="Padding" Value="5"/>
+ <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='25,1093'}"/>
+ </Style>
+ <Style TargetType="TextBlock" x:Key="baseText">
+ <Setter Property="HorizontalAlignment" Value="Left"/>
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Padding" Value="0,0,2,0"/>
+ <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='25,1093'}"/>
+ </Style>
+ <Brush x:Key="passedColor">#beed00</Brush>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <Grid dp:GridHelper.InnerBorder="1" dp:GridHelper.Space="2" x:Name="tableHeader"
+ dp:GridHelper.InnerBorderBrush="{DynamicResource CommonBoarder}">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition />
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+ <StackPanel Grid.RowSpan="3">
+ <StackPanel.Style>
+ <Style TargetType="StackPanel">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="Passed">
+ <Setter Property="Background" Value="{StaticResource passedColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Testing">
+ <Setter Property="Background" Value="Gold"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Failed">
+ <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Failed">
+ <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Ready">
+ <Setter Property="Background" Value="DeepSkyBlue"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </StackPanel.Style>
+
+ <Label>
+ <Label.Style>
+ <Style TargetType="Label">
+ <Setter Property="VerticalAlignment" Value="Center"/>
+ <Setter Property="Padding" Value="10,10,10,0"/>
+ <Setter Property="Margin" Value="0"/>
+ <Setter Property="FontFamily" Value="{StaticResource iconfont}"/>
+ <Setter Property="Foreground" Value="White"/>
+ <Setter Property="HorizontalContentAlignment" Value="Center"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='50,816'}"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="Passed">
+ <Setter Property="Content" Value=""/>
+ <Setter Property="Background" Value="{StaticResource passedColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Testing">
+ <Setter Property="Content" Value=""/>
+ <Setter Property="Background" Value="Gold"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Failed">
+ <Setter Property="Content" Value=""/>
+ <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Failed">
+ <Setter Property="Content" Value=""/>
+ <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Ready">
+ <Setter Property="Content" Value=""/>
+ <Setter Property="Background" Value="DeepSkyBlue"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Label.Style>
+ </Label>
+
+ <Label Content="{Binding Status}" VerticalAlignment="Center" VerticalContentAlignment="Center"
+ Padding="0" Margin="0"
+ HorizontalContentAlignment="Center"
+ Foreground="#FFF"
+ FontWeight="Bold"
+ FontSize="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='15,816'}"/>
+ </StackPanel>
+
+ <!--<Label HorizontalContentAlignment="Left" Grid.Column="1" Content="{Binding Status}"/>-->
+
+ <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="0" Grid.Column="1">
+ <StackPanel Orientation="Horizontal">
+ <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Total}"/>
+ <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding TotalNum}"/>
+ <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Pass}"/>
+ <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding PassNum}"/>
+ <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Fail}"/>
+ <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding FailNum}"/>
+ <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.PassRate}"/>
+ <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding PassRate,StringFormat={}{0:P2}}"/>
+ <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.ProbeCount}"/>
+ <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding ProbeCount}"/>
+ </StackPanel>
+ </Label>
+
+ <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="1" Grid.Column="1">
+ <StackPanel Orientation="Horizontal">
+ <Label Style="{StaticResource baseLabel}" Content="{DynamicResource Ts.CurrentSnr}"/>
+ <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding Barcode}"/>
+ <Label Style="{StaticResource baseLabel}" Content="{DynamicResource Ts.TimeSpan}"/>
+ <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding ElapseTime}"/>
+ </StackPanel>
+ </Label>
+ <Border x:Name="screenContainer" BorderThickness="0" Grid.Column="2" Grid.RowSpan="2" MouseLeftButtonDown="screenContainer_MouseLeftButtonDown">
+
+ </Border>
+ </Grid>
+
+ <Grid Grid.Row="2" x:Name="simplePanel" dp:GridHelper.Space="2" dp:GridHelper.SpaceIgnore="TOP">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Label VerticalContentAlignment="Center"
+ HorizontalContentAlignment="Center"
+ FontSize="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='50,816'}">
+ <Label.Style>
+ <Style TargetType="Label">
+ <Setter Property="Foreground" Value="#848286"/>
+ <Setter Property="Content" Value="{Binding CurrentStep}"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="Passed">
+ <Setter Property="Background" Value="{StaticResource passedColor}"/>
+ <Setter Property="Content" Value="--PASS--"/>
+ <Setter Property="Foreground" Value="#000"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="Failed">
+ <Setter Property="Background" Value="{DynamicResource DangerColor}"/>
+ <Setter Property="Content" Value="--FAIL--"/>
+ <Setter Property="Foreground" Value="#000"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Label.Style>
+ </Label>
+ <ProgressBar Grid.Row="1" MinHeight="20" Value="{Binding ProcessValue}"
+ Height="{Binding ActualHeight,ElementName=control,
+Converter={StaticResource hfc}, ConverterParameter='30,816'}"/>
+ <Label Grid.Row="1"
+ VerticalContentAlignment="Center"
+ HorizontalContentAlignment="Center"
+ Content="{Binding ProcessValueLabel}" Background="Transparent" Foreground="#000"/>
+ </Grid>
+
+ <Grid Grid.Row="2" x:Name="detailPanel">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+
+
+ <Canvas Grid.Row="0" x:Name="canvas" Background="Transparent">
+ <DataGrid Canvas.Left="0" Canvas.Top="0" Height="{Binding ActualHeight,ElementName=canvas}"
+ Width="{Binding ActualWidth,ElementName=canvas}"
+ Style="{StaticResource DefaultDataGrid}" Margin="2,0,2,2"
+ FontSize="{DynamicResource tableFontSize}"
+ dp:DataGridHelper.ShowRowNumber="True"
+ VirtualizingPanel.IsContainerVirtualizable="True"
+ VirtualizingPanel.IsVirtualizing="True"
+ VirtualizingPanel.VirtualizationMode="Recycling"
+ RowHeight="{StaticResource nanD}"
+ ColumnHeaderHeight="{Binding ActualWidth,ElementName=control,
+Converter={StaticResource fc}, ConverterParameter='32,1093,30,45'}"
+ HorizontalScrollBarVisibility="Disabled"
+ ItemsSource="{Binding Datasource}" x:Name="table">
+
+ <DataGrid.Columns>
+ <DataGridTextColumn Header="#"
+ ElementStyle="{StaticResource TableTextBlock}"
+ Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}"
+ Width="0.4*"/>
+ <DataGridTextColumn Width="2.5*" Binding="{Binding Name}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.Name}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn Width="0.8*" Binding="{Binding Unit}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.Unit}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn Width="2*" Binding="{Binding Value}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.Value}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn Width="1.5*" Binding="{Binding LimitDown}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.LimitDown}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn Width="1.5*" Binding="{Binding LimitUp}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.LimitUp}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTextColumn Width="0.6*" Binding="{Binding ConsumeTime}"
+ ElementStyle="{StaticResource TableTextBlock}">
+ <DataGridTextColumn.Header>
+ <TextBlock Text="{DynamicResource Test.ConsumeTime}"/>
+ </DataGridTextColumn.Header>
+ </DataGridTextColumn>
+ <DataGridTemplateColumn Width="1*">
+ <DataGridTemplateColumn.Header>
+ <TextBlock Text="{DynamicResource Test.Result}"/>
+ </DataGridTemplateColumn.Header>
+ <DataGridTemplateColumn.CellTemplate>
+ <DataTemplate>
+ <Grid x:Name="border" Margin="0">
+ <TextBlock x:Name="text" Margin="-1" Padding="0" VerticalAlignment="Center" Text="{Binding Result}"/>
+ </Grid>
+ <DataTemplate.Triggers>
+ <DataTrigger Binding="{Binding Result}" Value="Passed">
+ <Setter TargetName="border" Property="Background" Value="{StaticResource passedColor}"/>
+ <Setter TargetName="text" Property="Foreground" Value="#000"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Result}" Value="Failed">
+ <Setter TargetName="border" Property="Background" Value="{DynamicResource DangerColor}"/>
+ <Setter TargetName="text" Property="Foreground" Value="#000"/>
+ </DataTrigger>
+ </DataTemplate.Triggers>
+ </DataTemplate>
+ </DataGridTemplateColumn.CellTemplate>
+ </DataGridTemplateColumn>
+ </DataGrid.Columns>
+ </DataGrid>
+ </Canvas>
+
+ <Grid Grid.Row="1" Height="20" Margin="0">
+ <ProgressBar Grid.Row="3" x:Name="testProcess" Value="{Binding ProcessValue}"/>
+ <Label Width="45" DockPanel.Dock="Right" x:Name="testProcessLabel" Padding="0"
+ Content="{Binding ProcessValueLabel}"
+ VerticalContentAlignment="Center"/>
+ </Grid>
+
+ <Grid Background="#f1eded" Grid.Row="2" Height="25">
+ <DockPanel>
+ <StackPanel Margin="10,0,0,0" DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="8">
+ <StackPanel Orientation="Horizontal">
+ <Ellipse Width="10" Height="10">
+ <Ellipse.Style>
+ <Style TargetType="Ellipse">
+ <Setter Property="Fill" Value="LightGray"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding TcpServerStatus,ElementName=control}" Value="ERROR">
+ <Setter Property="Fill" Value="Red"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding TcpServerStatus,ElementName=control}" Value="RUNNING">
+ <Setter Property="Fill" Value="Green"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Ellipse.Style>
+ </Ellipse>
+ <TextBlock Text="{Binding TcpServerInfo}"/>
+ <Button Padding="0" Margin="0" BorderThickness="0" Click="Button_Click">馃攧</Button>
+ </StackPanel>
+ </StackPanel>
+ <Label Margin="10,0,0,0" Padding="0" FontSize="16" VerticalContentAlignment="Center" Content="{Binding SocketName}"/>
+ <Label Margin="20,0,0,0" Padding="0" FontSize="14" VerticalContentAlignment="Center" Content="{Binding CurrentStep}"/>
+
+ <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="0,0,5,0">
+ <!--<Button FontSize="12" x:Name="scannerSetting"
+ Margin="0,0,10,0" Click="Button_Click" Content="{DynamicResource App.ScannerSetting}">
+ <Button.Style>
+ <Style TargetType="Button" BasedOn="{StaticResource LinkButton}">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding IsAutoMode}" Value="True">
+ <Setter Property="Visibility" Value="Collapsed"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Button.Style>
+ </Button>-->
+
+ <Label Padding="0" FontSize="12"
+VerticalAlignment="Center"
+VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeCount}"/>
+ <Label Padding="0" FontSize="12" Margin="0,0,15,0"
+VerticalAlignment="Center"
+VerticalContentAlignment="Center">
+ <TextBlock>
+ <Run Text="{Binding ProbeCount}"/>
+ <Run Text="/"/>
+ <Run Text="{Binding ProbeWarn,Mode=OneWay}"/>
+ </TextBlock>
+ </Label>
+
+ <StackPanel x:Name="probeWarnPanel" Orientation="Horizontal" Visibility="Collapsed">
+ <Label x:Name="alarmLabel" Padding="0" FontSize="12"
+VerticalAlignment="Center"
+VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeAlarmCount}"/>
+ <Label x:Name="alarmValue" Padding="0" FontSize="12"
+VerticalAlignment="Center"
+VerticalContentAlignment="Center" Content="{Binding ProbeWarn,Mode=OneWay}"/>
+ </StackPanel>
+
+ <Button FontSize="12" Style="{StaticResource LinkButton}"
+ cdp:PermissionHelper.RequiredPermission="Permission:ClearProbeCount"
+ x:Name="clear" Margin="10,0,0,0" Click="Button_Click"
+ Content="{DynamicResource App.Clear}" Cursor=""/>
+ </StackPanel>
+
+ <Label x:Name="probeCountAlarm" Visibility="Collapsed" DockPanel.Dock="Left" Padding="30,0" FontSize="12" Foreground="Red"
+VerticalAlignment="Center"
+VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeAlarm}"/>
+ </DockPanel>
+ </Grid>
+ </Grid>
+
+ <Canvas Grid.Row="0" Grid.RowSpan="5" Visibility="Collapsed"
+ VirtualizingPanel.IsVirtualizing="False"
+ VirtualizingPanel.VirtualizationMode="Standard"
+ Background="#AAFFFFFF" x:Name="pausePanel" SizeChanged="pausePanel_Loaded">
+ <Border BorderBrush="#000" Background="#FFF" BorderThickness="0.5"
+ CornerRadius="5"
+ x:Name="visualModal">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition />
+ </Grid.ColumnDefinitions>
+ <Border Padding="10,5,0,5" Grid.ColumnSpan="2" Background="{DynamicResource WarningColor}"
+ CornerRadius="5,5,0,0">
+ <Label Padding="0" Margin="0" Foreground="#FFF" Content="{DynamicResource App.Warn}"/>
+ </Border>
+
+ <TextBlock Margin="20,20,20,0" Grid.Row="1"
+ FontSize="40"
+ FontFamily="/Panuon.WPF.UI;component/Resources/Fonts/#PanuonIcon"
+ Foreground="#F9D01A" Text=""/>
+
+ <Label Margin="0,20,20,0" Grid.Row="1" Grid.Column="1" Content="{DynamicResource Ts.PauseMsg}"/>
+ <StackPanel Margin="10" Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right">
+ <Button Style="{StaticResource ModalButton}" Tag="Cancel"
+ x:Name="cancelPauseBtn"
+ Click="Button_Click" Content="{DynamicResource App.Cancel}"/>
+ <Button Style="{StaticResource ModalButton}"
+ x:Name="continuePauseBtn"
+ Click="Button_Click"
+ Background="{DynamicResource WarningColor}"
+ Tag="OK" Content="{DynamicResource App.Continue}"/>
+ </StackPanel>
+ </Grid>
+ </Border>
+ </Canvas>
+ </Grid>
+</UserControl>
diff --git a/Plugin.StandardTest/SingleControl.xaml.cs b/Plugin.StandardTest/SingleControl.xaml.cs
new file mode 100644
index 0000000..fb4d9a7
--- /dev/null
+++ b/Plugin.StandardTest/SingleControl.xaml.cs
@@ -0,0 +1,916 @@
+锘縰sing HMI.Core.Entity;
+using HMI.Core.Manager;
+using HMI.Core.Setting;
+using HMI.Core.Util;
+using HMI.UI.Util;
+using HMI.Util;
+using NationalInstruments.TestStand.Interop.API;
+using Plugin.StandardTest.Entity;
+using Plugin.StandardTest.Setting;
+using SuperSocket.SocketBase.Config;
+using SuperSocket.SocketBase.Protocol;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Configuration;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Timers;
+using System.Windows;
+using System.Windows.Controls;
+using Util.ManualDebug;
+
+namespace Plugin.StandardTest
+{
+ /// <summary>
+ /// SingleControl.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class SingleControl : UserControl, INotifyPropertyChanged
+ {
+ #region ...
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void Set([CallerMemberName] string name = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
+ {
+ old = @new;
+ if (propertyName != null)
+ {
+ Set(propertyName);
+ }
+ }
+ #endregion
+ private int index;
+ private string CountIniPath;
+
+ private string status = "Ready";
+
+ private ScreenTouch[] screens;
+ private ScreenGrid screenGrid;
+
+ public string Status
+ {
+ get { return status; }
+ set { Set(ref status, value); }
+ }
+
+ private double totalNum;
+ public double TotalNum
+ {
+ get => totalNum;
+ private set
+ {
+ Set(ref totalNum, value);
+ }
+ }
+
+ public StandardTestUI parent { get; set; }
+
+ private int passNum;
+ public int PassNum
+ {
+ get => passNum;
+ private set { Set(ref passNum, value); }
+ }
+
+ private int failNum;
+ public int FailNum
+ {
+ get => failNum;
+ private set { Set(ref failNum, value); }
+ }
+
+ private double passRate;
+ public double PassRate
+ {
+ get => passRate;
+ private set
+ {
+ Set(ref passRate, value);
+ }
+ }
+
+ public string NumInfo
+ {
+ get => this.lang("Ts.NumInfo", TotalNum, PassNum, FailNum, string.Format("{0:0.00}", PassRate));
+ }
+
+ public string TestInfo
+ {
+ get => this.lang("Ts.TestInfo", Barcode, ElapseTime);
+ }
+
+ private string barcode = string.Empty;
+ public string Barcode
+ {
+ get { return barcode; }
+ set { Set(ref barcode, value); }
+ }
+
+ private string model = string.Empty;
+ public string Model
+ {
+ get { return model; }
+ set { Set(ref model, value); }
+ }
+
+ private string elapseTime = "00:00:00";
+ public string ElapseTime
+ {
+ get { return elapseTime; }
+ set
+ {
+ if (value == "Start Timing")
+ {
+ this.refreshTimer.Enabled = true;
+ TestStartTime = DateTime.Now;
+ StepStartTime = DateTime.Now;
+ }
+ else
+ {
+ this.refreshTimer.Enabled = false;
+ if (value != "Stop Timing")
+ {
+ Set(ref elapseTime, value + "s");
+ Set("TestInfo");
+ }
+ }
+ }
+ }
+
+ private string socketName = string.Empty;
+ public string SocketName
+ {
+ get { return socketName; }
+ set
+ {
+ Set(ref socketName, value);
+ }
+ }
+
+ private ObservableCollection<TestStep> _datasource = new ObservableCollection<TestStep>();
+ public ObservableCollection<TestStep> Datasource
+ {
+ get => _datasource;
+ set
+ {
+ Set(ref _datasource, value);
+ }
+ }
+
+ private DateTime TestStartTime = new DateTime();
+ private DateTime StepStartTime = new DateTime();
+ private TimeSpan TimeCount = new TimeSpan();
+
+ private System.Timers.Timer refreshTimer;
+
+ private string currentStep = "";
+ public string CurrentStep
+ {
+ get => currentStep;
+ set
+ {
+ Set(ref currentStep, value);
+ }
+ }
+
+ private int _probeCount;
+ public int ProbeCount
+ {
+ get => _probeCount;
+ set
+ {
+ Set(ref _probeCount, value);
+ try
+ {
+ if (ProbeWarn <= 0) probeCountAlarm.Visibility = Visibility.Collapsed;
+ else if (value >= ProbeWarn) probeCountAlarm.Visibility = Visibility.Visible;
+ else probeCountAlarm.Visibility = Visibility.Collapsed;
+ }
+ catch
+ {
+
+ }
+ }
+ }
+
+ /// <summary>
+ /// 杩涘害鍊�
+ /// </summary>
+ private int _processValue;
+ public int ProcessValue
+ {
+ get => _processValue;
+ set
+ {
+ Set(ref _processValue, value);
+ }
+ }
+
+ public string ProcessValueLabel
+ {
+ get
+ {
+ if (ProcessValue > 0) return $"{ProcessValue}%";
+ return "";
+ }
+ set
+ {
+ }
+ }
+
+ public void SetSimpleUI(bool flag)
+ {
+ if (flag)
+ {
+ simplePanel.Visibility = Visibility.Visible;
+ detailPanel.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ simplePanel.Visibility = Visibility.Collapsed;
+ detailPanel.Visibility = Visibility.Visible;
+ }
+ }
+
+ private int _probeWarn;
+ public int ProbeWarn
+ {
+ get => _probeWarn;
+ private set
+ {
+ Set(ref _probeWarn, value);
+ }
+ }
+
+ private bool isAutoMode;
+ public bool IsAutoMode
+ {
+ get => isAutoMode;
+ set
+ {
+ Set(ref isAutoMode, value);
+ }
+ }
+
+ public SingleControl(int Index, string CountIniPath, int probeWarn)
+ {
+ DataContext = this;
+ this.CountIniPath = CountIniPath;
+ this.index = Index;
+ ProbeWarn = probeWarn;
+
+ refreshTimer = new System.Timers.Timer()
+ {
+ Interval = 100
+ };
+ refreshTimer.Elapsed += RefreshTimer_Elapsed;
+ ReadCounter();
+ InitializeComponent();
+ RefreshProbeCount(null);
+ try
+ {
+ var value = ParameterManager.GetCommonParameterByName("AutoTestMode");
+ IsAutoMode = bool.Parse(value.Value);
+ }
+ catch
+ {
+ IsAutoMode = true;
+ }
+ }
+
+ private void RefreshTimer_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ TimeCount = DateTime.Now - TestStartTime;
+ elapseTime = string.Format("{0:00}:{1:00}:{2:00}", TimeCount.Hours, TimeCount.Minutes, TimeCount.Seconds);
+ Set("ElapseTime");
+ Set("TestInfo");
+ }
+
+ /// <summary>
+ /// 鑾峰彇TS鐨勬瘡姝ユ祴璇曠殑鏁版嵁娣诲姞鍒拌〃鏍�
+ /// 椤圭洰鍚嶇О锛屾祴璇曞弬鏁帮紝娴嬭瘯鍊硷紝涓婇檺锛屼笅闄愶紝鍗曚綅锛岀粨鏋�
+ /// </summary>
+ /// <param name="TS_Data"></param>
+ public void AddTestData(string name, string step_name, string value, string HightLimit, string LowLimit, string unit, string Status)
+ {
+ try
+ {
+ var data = new TestStep
+ {
+ Name = name,
+ Step = step_name,
+ Value = value,
+ LimitUp = HightLimit,
+ LimitDown = LowLimit,
+ Unit = unit,
+ Result = Status
+ };
+ table.Dispatcher.Invoke(new Action(() =>
+ {
+ var now = DateTime.Now;
+ var span = now - StepStartTime;
+ StepStartTime = now;
+ data.ConsumeTime = $"{(span.TotalMilliseconds / 1000d).ToString("F2")}s";
+ Datasource.Add(data);
+ table.ScrollIntoView(data);
+ }));
+ }
+ catch (Exception ex)
+ {
+ //灞忚斀閿欒
+ }
+ }
+
+ public void UpdateStatus(string status)
+ {
+ if (status == "Passed")
+ {
+ UpdateTestCounter(true);
+ }
+ else if (status == "Error" || status == "Failed")
+ {
+ UpdateTestCounter(false);
+ }
+ else if (Status != "Testing" && status == "Testing")
+ {
+ UpdateProbeCount(ProbeCount + 1);
+ }
+ Status = status;
+ }
+
+ public void IniTestData()
+ {
+ Datasource.Clear();
+ }
+
+ /// <summary>
+ /// 璁剧疆娴嬭瘯杩涘害鏉�
+ /// </summary>
+ /// <param name="ProcessVal"></param>
+ public void SetTestProcess(int ProcessVal)
+ {
+ ProcessValue = ProcessVal;
+ Set("ProcessValueLabel");
+ }
+
+ private void UserControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ StartTcpServer();
+ this.UpdateTestCounter(null);
+ Loaded -= UserControl_Loaded;
+ }
+
+ private void UpdateTestCounter(bool? result)
+ {
+ if (result == true)
+ {
+ PassNum += 1;
+ //Interlocked.Increment(ref passNum);
+ //Set("PassNum");
+ }
+ else if (result == false)
+ {
+ FailNum += 1;
+ //Interlocked.Increment(ref failNum);
+ //Set("FailNum");
+ }
+
+ TotalNum = PassNum + FailNum;
+ if (TotalNum != 0)
+ {
+ PassRate = ((double)PassNum / TotalNum);
+ }
+ else
+ {
+ PassRate = 0.0;
+ }
+ Set("NumInfo");
+ this.WriteCounter();
+ }
+
+ private void WriteCounter()
+ {
+ IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "Pass",
+ this.passNum.ToString(), this.CountIniPath);
+ IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "Fail", this.failNum.ToString(),
+ this.CountIniPath);
+ }
+
+ private void ReadCounter()
+ {
+ try
+ {
+ this.PassNum = Convert.ToInt32(IniFileHelper.ReadIniData("Socket" + this.index.ToString(), "Pass", "", CountIniPath));
+ this.FailNum = Convert.ToInt32(IniFileHelper.ReadIniData("Socket" + this.index.ToString(), "Fail", "", CountIniPath));
+ }
+ catch (Exception ex)
+ {
+ //do nothing
+ }
+ }
+
+ public void ResetCounter()
+ {
+ this.PassNum = 0;
+ this.FailNum = 0;
+ this.UpdateTestCounter(null);
+ }
+
+ public void WriteToExcel(string path)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ try
+ {
+ string suffix = Path.GetExtension(path);
+ if (!string.Equals(".xlsx", suffix, StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(".xls", suffix, StringComparison.OrdinalIgnoreCase))
+ {
+ path = path.Replace(suffix, ".xls");
+ }
+
+ FileInfo fi = new FileInfo(path);
+ if (!fi.Directory.Exists)
+ {
+ fi.Directory.Create(); //鏂囦欢涓嶅瓨鍦紝鍒涘缓鏂囦欢
+ }
+ List<string[]> rows = new List<string[]>();
+ rows.Add(new string[] { "娴嬭瘯鏃堕棿", "娴嬭瘯浜�", "娴嬭瘯璁惧", "鏉″舰鐮�", "娴嬭瘯缁撴灉" });
+ rows.Add(new string[] { DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), UserInfo.User.Name, ParamSetting.StationName, Barcode, Status });
+ rows.Add(new string[] { "椤圭洰鍚嶇О", "娴嬭瘯鍙傛暟", "娴嬭瘯鍊�", "涓婇檺", "涓嬮檺", "鍗曚綅", "缁撴灉" });
+ if (Datasource != null && Datasource.Count > 0)
+ {
+ foreach (var data in Datasource)
+ {
+ rows.Add(new string[] { data.Name, data.Step, data.Value, data.LimitUp, data.LimitDown, data.Unit, data.Result });
+ }
+ }
+ ExcelHelper.ExportExcel(path, rows);
+ }
+ catch (Exception ex)
+ {
+ StandardTestUI.logger.Error("Write CSV File Failed.", ex);
+ }
+ });
+ }
+
+
+ public void WriteToCSV(string path)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ try
+ {
+
+ FileInfo fi = new FileInfo(path);
+ if (!fi.Directory.Exists)
+ {
+ fi.Directory.Create(); //鏂囦欢涓嶅瓨鍦紝鍒涘缓鏂囦欢
+ }
+
+ using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
+ using (StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8))
+ {
+ string title = "椤圭洰鍚嶇О,娴嬭瘯鍙傛暟,娴嬭瘯鍊�,涓婇檺,涓嬮檺,鍗曚綅,缁撴灉";
+ sw.WriteLine(title);
+
+ if (Datasource != null && Datasource.Count > 0)
+ {
+ foreach (var data in Datasource)
+ {
+ sw.WriteLine($"{data.Name},{data.Step},{data.Value},{data.LimitUp},{data.LimitDown},{data.Unit},{data.Result}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ StandardTestUI.logger.Error("Write CSV File Failed.", ex);
+ }
+ });
+ }
+
+ #region TCP鐩稿叧
+
+ private string tcpServerStatus;
+ public string TcpServerStatus
+ {
+ get => tcpServerStatus;
+ set
+ {
+ Set(ref tcpServerStatus, value);
+ }
+ }
+
+ private string tcpServerInfo;
+ public string TcpServerInfo
+ {
+ get => tcpServerInfo;
+ set
+ {
+ Set(ref tcpServerInfo, value);
+ }
+ }
+
+ private TcpServer tcpServer;
+
+ private int GetConfigPort()
+ {
+ try
+ {
+ var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig");
+ if (customConfig.ContainsKey("PortStart"))
+ {
+ int port = int.Parse(customConfig["PortStart"].ToString());
+ return port;
+ }
+ else
+ {
+ return 6660;
+ }
+ }
+ catch (Exception ex)
+ {
+ return 6660;
+ }
+ }
+
+ private void StartTcpServer()
+ {
+ int port = GetConfigPort();
+ var serverConfig = new ServerConfig()
+ {
+ MaxRequestLength = 1024 * 1024,
+ Port = port + index
+ };
+ try
+ {
+ if (tcpServer == null)
+ {
+ tcpServer = new TcpServer();
+ tcpServer.NewRequestReceived += TcpServer_NewRequestReceived;
+ if (!tcpServer.Setup(serverConfig))
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed"));
+ return;
+ }
+ if (!tcpServer.Start())
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed"));
+ return;
+ }
+ ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ }
+ else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running)
+ {
+ if (!tcpServer.Setup(serverConfig))
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed"));
+ return;
+ }
+ if (!tcpServer.Start())
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed"));
+ return;
+ }
+ ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ }
+ }
+ catch (Exception ex)
+ {
+ ChangeTcpServerStatus("ERROR", $"{ex.Message}");
+ tcpServer?.Stop();
+ tcpServer = null;
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊Tcp鏈嶅姟鍣ㄧ姸鎬�
+ /// </summary>
+ /// <param name="status"></param>
+ /// <param name="info"></param>
+ private void ChangeTcpServerStatus(string status, string info)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ TcpServerStatus = status;
+ TcpServerInfo = info;
+ });
+ }
+
+ /// <summary>
+ /// 鍋滄Tcp鏈嶅姟鍣�
+ /// </summary>
+ private void StopTcpServer()
+ {
+ try
+ {
+ tcpServer?.Stop();
+ tcpServer = null;
+ }
+ catch (Exception ex)
+ {
+
+ }
+ }
+
+ private void TcpServer_NewRequestReceived(TcpSession session, StringRequestInfo requestInfo)
+ {
+ string msg = requestInfo.Body;
+ StandardTestUI.logger.Debug($"TCP Data From {session.RemoteEndPoint}: {msg}.");
+ string[] datas_10015 = msg.Split(';');
+ int cellIndex = int.Parse(datas_10015[1]);
+
+ string strs = datas_10015[2];
+ string[] values = strs.Split(',');
+ Dispatcher.Invoke(() =>
+ {
+ AddTestData("", values[0], values[2], values[4], values[5], values[3], values[6]);
+ //new Info
+ //{
+ // step = values[0],
+ // value = values[2],
+ // unit = values[3],
+ // limitUp = values[4],
+ // limitDown = values[5],
+ // result = values[6],
+ // updateTime = values[7]
+ //}
+ });
+ //session.Send("1");
+ }
+ #endregion
+
+ public void ShowPausePanel()
+ {
+ pausePanel.Visibility = Visibility.Visible;
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Button btn = sender as Button;
+ switch (btn.Name)
+ {
+ case "clear":
+ UpdateProbeCount(0);
+ break;
+ case "cancelPauseBtn":
+ parent.testExecutionWrapper.SetStationGlobalValue($"StationState.IsContinue_{index}", 1, false);
+ pausePanel.Visibility = Visibility.Collapsed;
+ break;
+ case "continuePauseBtn":
+ parent.testExecutionWrapper.SetStationGlobalValue($"StationState.IsContinue_{index}", 1, true);
+ pausePanel.Visibility = Visibility.Collapsed;
+ break;
+ case "scannerSetting":
+ //ScannerSetting setting = new ScannerSetting()
+ //{
+ // Owner = Window.GetWindow(this)
+ //};
+ //setting.Open(true, $"Socket{index}", CountIniPath);
+ ShowScannerPanel();
+ break;
+ case "scannerOkBtn":
+ SendSnr2Ts();
+ break;
+ default:
+ break;
+ }
+ }
+ public void ShowScannerPanel()
+ {
+ //scannerPanel.Visibility = Visibility.Visible;
+ }
+
+ private void SendSnr2Ts()
+ {
+
+ }
+
+ private void pausePanel_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender == pausePanel)
+ {
+ var can = pausePanel;
+ var modal = visualModal;
+ Canvas.SetLeft(modal, (pausePanel.ActualWidth - visualModal.ActualWidth) / 2);
+ Canvas.SetTop(modal, (pausePanel.ActualHeight - visualModal.ActualHeight) / 2);
+ }
+ else
+ {
+ //var can = scannerPanel;
+ //var modal = scannerVisualModal;
+ //Canvas.SetLeft(modal, (scannerPanel.ActualWidth - scannerVisualModal.ActualWidth) / 2);
+ //Canvas.SetTop(modal, (scannerPanel.ActualHeight - scannerVisualModal.ActualHeight) / 2);
+ }
+ }
+
+ public void RefreshScreen(int[][] configs)
+ {
+ //foreach (UIElement item in canvas.Children)
+ //{
+ // if (item is ScreenTouch)
+ // {
+ // canvas.Children.Remove(item);
+ // }
+ //}
+ screenContainer.Child = null;
+
+
+ screenGrid = new ScreenGrid(1, configs.Length);
+
+ screens = new ScreenTouch[configs.Length];
+
+ int space = 2 * (screens.Length + 1);
+ for (int i = 0; i < configs.Length; i++)
+ {
+ var screen = new ScreenTouch(configs[i][0], configs[i][1])
+ {
+ Width = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length, 0),
+ Height = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9, 0)
+ };
+ screens[i] = screen;
+ screenGrid.AddScreen(screen, 0, i);
+ }
+ screenContainer.Child = screenGrid;
+
+ //canvas.Children.Add(screens[0]);
+ //Canvas.SetBottom(screens[0], 0);
+ //Canvas.SetRight(screens[0], 0);
+ //Panel.SetZIndex(screens[0], 999);
+ }
+
+ public void DrawScreenPoint(int index, int x, int y)
+ {
+ screens[index].DrawPoint(x, y);
+ }
+
+ /// <summary>
+ /// 缁樺浘涔嬪墠娓呯┖宸叉湁鐨�
+ /// </summary>
+ /// <param name="index"></param>
+ /// <param name="points"></param>
+ public void DrawScreenPoints(int index, int[][] points)
+ {
+ screens[index].DrawPoints(points);
+ }
+
+ /// <summary>
+ /// 缁樺浘涔嬪墠涓嶆竻绌哄凡鏈夌殑
+ /// </summary>
+ /// <param name="index"></param>
+ /// <param name="points"></param>
+ public void DrawNewScreenPoints(int index, int[][] points)
+ {
+ screens[index].DrawNewPoints(points);
+ }
+
+ public void ClearScreenPoint(int index)
+ {
+ screens[index].Clear();
+ }
+
+ public void RefreshProbeCount(ProductData product)
+ {
+ try
+ {
+ var num = IniFileHelper.ReadIniData($"Socket{this.index.ToString()}", "ProbeCount", "0", this.CountIniPath);
+ this.ProbeCount = int.Parse(num);
+ }
+ catch (Exception ex)
+ {
+ this.ProbeCount = 0;
+ }
+ }
+
+ /// <summary>
+ /// 鏇存柊鎺㈤拡浣跨敤娆℃暟
+ /// </summary>
+ /// <param name="count"></param>
+ private void UpdateProbeCount(int count)
+ {
+ /// 鍐欏埌閰嶇疆鏂囦欢
+ IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "ProbeCount", count.ToString(), this.CountIniPath);
+ ProbeCount = count;
+ }
+
+ public void RefreshProduct(ProductData product)
+ {
+ if (product == null)
+ {
+ screenContainer.Child = null;
+ return;
+ }
+ try
+ {
+ string screenSize = ProductManager.GetParameterByProductAndName(product.Name, "ScreenTouch")?.Value;
+ if (string.IsNullOrEmpty(screenSize))
+ {
+ screenContainer.Child = null;
+ return;
+ }
+ string[] arr = screenSize.Split(';');
+ int[][] configs = new int[arr.Length][];
+
+ for (int i = 0; i < arr.Length; i++)
+ {
+ string[] size = arr[i].Split(',');
+ configs[i] = new int[2] { int.Parse(size[0]), int.Parse(size[1]) };
+ }
+ RefreshScreen(configs);
+ }
+ catch (Exception ex)
+ {
+ StandardTestUI.logger.Error("RefreshProduct Failed.", ex);
+ screenContainer.Child = null;
+ }
+ }
+
+ private void control_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (screenGrid == null)
+ {
+ return;
+ }
+ //screenGrid.Height = tableHeader.ActualHeight - 2;
+ //screenGrid.Width = tableHeader.ActualHeight / 9 * 16 - 2;
+
+ int space = 2 * (screens.Length + 1);
+
+ for (int i = 0; i < screens.Length; i++)
+ {
+ var screen = screens[i];
+ screen.Width = Math.Max(Math.Floor((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length), 0);
+ screen.Height = Math.Max(Math.Floor(screen.Width / 16 * 9), 0);
+ }
+ }
+
+ private void screenContainer_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (e.ClickCount == 2)
+ {
+ new ScreenTouchWin(screens)
+ {
+ Owner = Window.GetWindow(this)
+ }.ShowDialog();
+ }
+ }
+
+ private void control_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (parent.EnableManualDebug)
+ {
+ ContextMenu contextMenu = new ContextMenu();
+ contextMenu.Closed += ContextMenu_Closed;
+ var item = new MenuItem
+ {
+ IsCheckable = false,
+ Name = "manualDebugItem",
+ Header = this.lang("鎵嬪姩璋冭瘯"),
+ };
+ item.Click += Item_Click; ;
+ contextMenu.Items.Add(item);
+ this.ContextMenu = contextMenu;
+ }
+ else
+ {
+
+ this.ContextMenu = null;
+ }
+ }
+
+ private void Item_Click(object sender, RoutedEventArgs e)
+ {
+ string manualDebugSeq = ParameterManager.GetCommonParameterByName("ManualDebugSeqPath")?.Value;
+ if (string.IsNullOrEmpty(manualDebugSeq))
+ {
+ MsgBoxHelper2.Error("璇峰厛璁剧疆鎵嬪姩璋冭瘯搴忓垪璺緞[ManualDebugSeqPath]!");
+ return;
+ }
+ string modelPath = parent.tsModelPath;
+ var win = new ManualDebugWindow(manualDebugSeq, modelPath, false)
+ {
+ Title = $"宸ヤ綅{this.index}鎵嬪姩璋冭瘯",
+
+ Owner = Window.GetWindow(this),
+ OvenIndex = index,
+ OvenName = "",
+ CellCount = parent.tsSockets.Count,
+
+ CellIndex = 0,
+ CellName = "",
+ };
+ win.ShowDialog();
+ }
+
+ private void ContextMenu_Closed(object sender, RoutedEventArgs e)
+ {
+ this.ContextMenu = null;
+ }
+ }
+}
diff --git a/Plugin.StandardTest/StandardTestUI.xaml b/Plugin.StandardTest/StandardTestUI.xaml
new file mode 100644
index 0000000..2addbb8
--- /dev/null
+++ b/Plugin.StandardTest/StandardTestUI.xaml
@@ -0,0 +1,315 @@
+锘�<hb:PluginMaskControl x:Class="Plugin.StandardTest.StandardTestUI"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ mc:Ignorable="d"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI"
+ xmlns:pw="https://opensource.panuon.com/wpf"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib"
+ xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI"
+ Padding="2,5,2,5"
+ Loaded="PluginControl_Loaded"
+ SizeChanged="PluginControl_SizeChanged"
+ Background="{DynamicResource WorkspaceBackground}"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <sys:Double x:Key="tableFontSize">20</sys:Double>
+ <Style x:Key="FormComboxStyle" TargetType="ComboBox" BasedOn="{StaticResource FormComboxStyle}">
+ <Setter Property="Height" Value="28"/>
+ </Style>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid>
+ <Grid>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition />
+ <ColumnDefinition Width="Auto"/>
+ </Grid.ColumnDefinitions>
+
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost>
+
+ <DockPanel Grid.Row="0" Grid.ColumnSpan="2" Margin="5,0,5,0" LastChildFill="False">
+ <DockPanel.Resources>
+ <Style TargetType="CheckBox">
+ <Setter Property="FontSize" Value="20"/>
+ <Setter Property="VerticalContentAlignment" Value="Center"/>
+ </Style>
+ </DockPanel.Resources>
+
+ <StackPanel Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="10">
+ <pu:FormGroup HeaderHeight="Auto" HeaderPadding="0" x:Name="selectModelPanel" Visibility="Collapsed">
+ <pu:FormGroup.Header>
+ <TextBlock FontSize="20" Foreground="#000" Text="{DynamicResource Ts.SelectProductModel}"/>
+ </pu:FormGroup.Header>
+ <ComboBox Style="{StaticResource FormComboxStyle}" MinWidth="180" ItemsSource="{Binding Products}"
+DisplayMemberPath="Name" SelectedValuePath="Name"
+SelectedItem="{Binding SelectedProduct}" x:Name="CbProduct"/>
+ </pu:FormGroup>
+
+ <pu:FormGroup HeaderHeight="Auto" HeaderPadding="0" x:Name="autoModelPanel" Visibility="Collapsed">
+ <pu:FormGroup.Header>
+ <TextBlock FontSize="20" Foreground="#000" Text="褰撳墠浜у搧"/>
+ </pu:FormGroup.Header>
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" MinWidth="180" Text="{Binding CurrentProductModel}"/>
+ </pu:FormGroup>
+
+ <CheckBox x:Name="CbEnableMes"
+ IsChecked="{Binding EnableMes,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ Content="{DynamicResource Ts.EnableMes}"
+ />
+ <CheckBox x:Name="CbEnableNGPause"
+ IsChecked="{Binding EnableNGPause,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ Content="{DynamicResource Ts.EnableNGPause}"
+ />
+ <CheckBox x:Name="CbEnableDebugMode"
+ IsChecked="{Binding EnableDebugMode,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ Content="{DynamicResource Ts.DebugMode}"/>
+
+ <CheckBox x:Name="CbDisableScanner"
+IsChecked="{Binding DisableScanner,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+Content="{DynamicResource Ts.BlockScanner}"/>
+
+ <CheckBox x:Name="CbEnableSimpleMode"
+IsChecked="{Binding EnableSimpleMode,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+Content="{DynamicResource Ts.EnableSimpleMode}"/>
+
+ <CheckBox x:Name="CbUI_ShowInfoDock"
+IsChecked="{Binding UI_ShowInfoDock,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+Content="{DynamicResource Ts.UI_ShowInfoDock}"/>
+
+ <CheckBox FontSize="20" x:Name="Cb_ManualDebug" Visibility="Collapsed"
+ IsChecked="{Binding EnableManualDebug,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ VerticalContentAlignment="Center" Content="鎵嬪姩璋冭瘯" />
+
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5">
+ <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0"
+ Content="{DynamicResource HMI.User.Code}" />
+ <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0"
+ Content="{Binding CurrentUserCode}" />
+ </StackPanel>
+
+
+ <Label x:Name="workorderPanel" VerticalContentAlignment="Center" Visibility="Collapsed" Padding="0" Margin="0" MouseDoubleClick="Label_MouseDoubleClick">
+ <StackPanel Orientation="Horizontal"
+ dp:StackPanelHelper.ChildSpace="5">
+ <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0"
+ Content="{DynamicResource Ts.WorkOrder}"/>
+ <TextBlock x:Name="TbWorkOrder" FontSize="20" MinWidth="50" Text="{Binding CurrentWorkorder}"/>
+ <!--<ui:EditBox FontSize="20" x:Name="workOrderEditor" MinWidth="50"
+ CanEdit="True"
+ ShowEmpty="True"
+ PreEditing="RequirePassword"
+ Value="{Binding CurrentWorkorder,Mode=TwoWay}"/>-->
+ </StackPanel>
+ </Label>
+
+ <!--<StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5">
+ <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" Content="瀛椾綋澶у皬"/>
+ <ComboBox Name="CbFontSize" Width="60" ItemsSource="{Binding FontSizeSource}"
+ SelectionChanged="CbFontSize_SelectionChanged"/>
+ </StackPanel>-->
+ </StackPanel>
+
+
+
+ <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10">
+ <Button Style="{StaticResource SuccessButton}"
+x:Name="BtnStartSeq" Click="Button_Click"
+Content="{DynamicResource Ts.BtnStartSeq}"/>
+ <Button Grid.Row="1" Style="{StaticResource DangerButton}" IsEnabled="False"
+x:Name="BtnStopSeq" Click="Button_Click"
+Content="{DynamicResource Ts.BtnStopSeq}"/>
+ <Button Grid.Row="2" Style="{StaticResource PrimaryButton}"
+x:Name="BtnClearNum" Click="Button_Click"
+Content="{DynamicResource Ts.BtnClearNum}"/>
+ <Button Grid.Row="3"
+x:Name="BtnSample" Click="Button_Click">
+ <Button.Style>
+ <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding SampleStatus}" Value="True">
+ <Setter Property="Content" Value="{DynamicResource Ts.BtnIsSample}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding SampleStatus}" Value="False">
+ <Setter Property="Content" Value="{DynamicResource Ts.BtnNotSample}"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Button.Style>
+ </Button>
+ </StackPanel>
+ </DockPanel>
+
+ <Grid dp:GridHelper.Space="5" Grid.Row="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+
+ <UniformGrid x:Name="panel" Grid.Row="1" Columns="2" dp:UniformGridHelper.InnerBorder="1"
+ dp:UniformGridHelper.InnerBorderBrush="#3a3a3a">
+
+
+ </UniformGrid>
+ <!--<Border x:Name="alarmPanel" Height="80" VerticalAlignment="Bottom" Visibility="Visible" Background="Red"/>
+ <Border Grid.Row="1" x:Name="alarmPanel" Visibility="{Binding AlarmVisiable,Converter={StaticResource vbConverter}}"
+ Height="50" Background="Red"/>-->
+ </Grid>
+
+
+
+ <Grid Grid.Row="1" Grid.Column="1" x:Name="detailPanel">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition />
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+
+ <!--<GroupBox>
+ <pu:FormGroup Orientation="Vertical"
+ HeaderHeight="Auto"
+ HeaderPadding="0" Header="{DynamicResource Ts.SelectProductModel}">
+ <ComboBox Style="{StaticResource FormComboxStyle}"
+ ItemsSource="{Binding Products}"
+ DisplayMemberPath="Name" SelectedValuePath="Name"
+ SelectedItem="{Binding SelectedProduct}"
+ x:Name="CbProduct"/>
+ </pu:FormGroup>
+ </GroupBox>-->
+
+ <GroupBox Grid.Row="2">
+ <StackPanel>
+ <pu:FormGroup Header="{DynamicResource Ts.MesStatus}" GroupName="Number">
+ <Label Grid.Row="1">
+ <Label.Style>
+ <Style TargetType="Label">
+ <Setter Property="Foreground" Value="#FFF"/>
+ <Setter Property="HorizontalContentAlignment" Value="Center"/>
+ <Setter Property="FontSize" Value="16"/>
+ <Setter Property="FontWeight" Value="Bold"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding EnableMes}" Value="True">
+ <Setter Property="Background" Value="Green"/>
+ <Setter Property="Content" Value="{DynamicResource Ts.MesStatus.Enable}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding EnableMes}" Value="False">
+ <Setter Property="Background" Value="Red"/>
+ <Setter Property="Content" Value="{DynamicResource Ts.MesStatus.Disable}"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Label.Style>
+ </Label>
+ </pu:FormGroup>
+ <pu:FormGroup Header="{DynamicResource Ts.Total}" GroupName="Number">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True"
+ x:Name="TbTotal"/>
+ </pu:FormGroup>
+ <pu:FormGroup Header="{DynamicResource Ts.Pass}" GroupName="Number">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True"
+ x:Name="TbPass"/>
+ </pu:FormGroup>
+ <pu:FormGroup Header="{DynamicResource Ts.Fail}" GroupName="Number">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True"
+ x:Name="TbFail"/>
+ </pu:FormGroup>
+ <pu:FormGroup Header="{DynamicResource Ts.PassRate}" GroupName="Number">
+ <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True"
+ x:Name="TbPassRate"/>
+ </pu:FormGroup>
+ </StackPanel>
+ </GroupBox>
+
+ <GroupBox Grid.Row="3">
+ <lvc:PieChart Grid.Row="3" x:Name="pie" Grid.Column="1" Background="#FFF" BorderThickness="1" BorderBrush="{DynamicResource CommonBoarder}"
+Title="{Binding Chart1Title}"
+TooltipTextPaint="{Binding TooltipTextPaint}"
+Padding="0"
+Margin="2"
+Series="{Binding Chart1_series}"
+LegendPosition="Top">
+ </lvc:PieChart>
+ </GroupBox>
+
+ <!-- <GroupBox Grid.Row="4">
+ <Grid dp:GridHelper.Space="20">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition/>
+ <RowDefinition/>
+ <RowDefinition/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <Button Style="{StaticResource SuccessButton}"
+ x:Name="BtnStartSeq" Click="Button_Click"
+ Content="{DynamicResource Ts.BtnStartSeq}"/>
+ <Button Grid.Row="1" Style="{StaticResource DangerButton}" IsEnabled="False"
+ x:Name="BtnStopSeq" Click="Button_Click"
+ Content="{DynamicResource Ts.BtnStopSeq}"/>
+ <Button Grid.Row="2" Style="{StaticResource PrimaryButton}"
+ x:Name="BtnClearNum" Click="Button_Click"
+ Content="{DynamicResource Ts.BtnClearNum}"/>
+ <Button Grid.Row="3"
+ x:Name="BtnSample" Click="Button_Click">
+ <Button.Style>
+ <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding SampleStatus}" Value="True">
+ <Setter Property="Content" Value="{DynamicResource Ts.BtnIsSample}"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding SampleStatus}" Value="False">
+ <Setter Property="Content" Value="{DynamicResource Ts.BtnNotSample}"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Button.Style>
+ </Button>
+
+ <pu:Switch Grid.Row="3" BoxWidth="120"
+ CheckedBackground="{DynamicResource PrimaryColor}"
+ CheckedBorderBrush="#36abff"
+ ToggleBrush="#030303"
+ CheckedForeground="White"
+ CheckedToggleBrush="#FFFFFF"
+ IsChecked="{Binding SampleStatus,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ ToggleSize="25" Cursor="Hand" >
+ <pu:Switch.BoxTemplate>
+ <DataTemplate>
+ <TextBlock x:Name="text"
+ Text="{Binding Converter={StaticResource lc},ConverterParameter='Ts.BtnNotSample'}"
+ Padding="30,0"
+ VerticalAlignment="Center" HorizontalAlignment="Center"/>
+ <DataTemplate.Triggers>
+ <DataTrigger Binding="{Binding IsChecked,
+ RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=pu:Switch,AncestorLevel=1}}"
+ Value="True">
+ <Setter TargetName="text" Property="Text"
+ Value="{Binding Converter={StaticResource lc},ConverterParameter='Ts.BtnIsSample'}"/>
+ <Setter TargetName="text" Property="Foreground" Value="{DynamicResource PrimaryColorContract}"/>
+ </DataTrigger>
+ </DataTemplate.Triggers>
+ </DataTemplate>
+ </pu:Switch.BoxTemplate>
+ </pu:Switch>
+ </Grid>
+ </GroupBox> -->
+ </Grid>
+ </Grid>
+ </Grid>
+</hb:PluginMaskControl>
diff --git a/Plugin.StandardTest/StandardTestUI.xaml.cs b/Plugin.StandardTest/StandardTestUI.xaml.cs
new file mode 100644
index 0000000..6c910a3
--- /dev/null
+++ b/Plugin.StandardTest/StandardTestUI.xaml.cs
@@ -0,0 +1,1906 @@
+锘縰sing HMI.Base;
+using HMI.Core.Api;
+using HMI.Core.Entity;
+using HMI.Core.Manager;
+using HMI.Core.Setting;
+using HMI.Core.Util;
+using HMI.Simple.Util;
+using HMI.UI.Util;
+using HMI.Util;
+using LiveChartsCore;
+using LiveChartsCore.SkiaSharpView;
+using LiveChartsCore.SkiaSharpView.Painting;
+using log4net;
+using NationalInstruments.TestStand.Interop.API;
+using NPOI.HSSF.UserModel;
+using NPOI.SS.UserModel;
+using Plugin.TsSupport;
+using SkiaSharp;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace Plugin.StandardTest
+{
+ /// <summary>
+ /// UserControl1.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class StandardTestUI : PluginMaskControl
+ {
+ #region TestStand浜嬩欢
+ /// <summary>
+ /// TS 寮曟搸
+ /// </summary>
+ //private Engine engine = null;
+
+ ///// <summary>
+ ///// 娴嬭瘯搴忓垪
+ ///// </summary>
+ //private SequenceFile testSeq = null;
+
+ ///// <summary>
+ ///// 娴嬭瘯妯″瀷搴忓垪
+ ///// </summary>
+ //private SequenceFile modeSeq = null;
+
+ //private Execution execution = null;
+
+ private string testSeqFile = string.Empty;
+
+ public ITestExecutionWrapper testExecutionWrapper { get; private set; }
+ #endregion
+
+ public static ILog logger { get; private set; }
+ public List<SingleControl> tsSockets { get; private set; } = new List<SingleControl>();
+ /// <summary>
+ /// 鏍蜂欢妯″紡鐘舵��
+ /// </summary>
+ private bool sampleStatus;
+ public bool SampleStatus
+ {
+ get { return sampleStatus; }
+ set
+ {
+ Set(ref sampleStatus, value);
+ SampleTest(value);
+ }
+ }
+
+ private string _currentUserCode;
+
+ public string CurrentUserCode
+ {
+ get { return _currentUserCode; }
+ set { Set(ref _currentUserCode, value); }
+ }
+
+
+ private bool isLoadOK = false;
+
+ private int PassAmount = 0;
+ private int FailAmount = 0;
+
+ /// <summary>
+ /// Socket鏁伴噺
+ /// </summary>
+ private int socketNum = 1;
+
+ private ProductData selectedProduct;
+
+ private MSMQHelper mSMQHelper = new MSMQHelper("HMI_Alarm");
+
+ public ProductData SelectedProduct
+ {
+ get { return selectedProduct; }
+ set
+ {
+ Set(ref selectedProduct, value);
+ CurrentProductModel = value?.Name;
+ if (tsSockets != null)
+ {
+ foreach (SingleControl socket in tsSockets)
+ {
+ socket.RefreshProduct(value);
+ // socket.RefreshProbeCount(value);
+ }
+ }
+ SetTestSeqFile(value);
+ }
+ }
+
+ private string currentProductModel;
+ public string CurrentProductModel
+ {
+ get => currentProductModel;
+ set
+ {
+ Set(ref currentProductModel, value);
+ }
+ }
+
+ public override void SetLogger(ILog logger)
+ {
+ base.SetLogger(logger);
+ StandardTestUI.logger = logger;
+ }
+
+ private ObservableCollection<ProductData> products;
+
+ public ObservableCollection<ProductData> Products
+ {
+ get { return products; }
+ set
+ {
+ Set(ref products, value);
+ }
+ }
+
+ public List<double> FontSizeSource { get; set; } = new List<double> {
+ 10,11,12,13,14,15,16,17,18,19,20
+ };
+
+ private string CountIniPath;
+ private bool IsOptRequirePassword;
+
+ private bool alarmVisiable;
+ public bool AlarmVisiable
+ {
+ get => alarmVisiable;
+ set
+ {
+ Set(ref alarmVisiable, value);
+ }
+ }
+
+ private bool _enableMes;
+ public bool EnableMes
+ {
+ get => _enableMes;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("EnableMes", value))
+ {
+ Set(ref _enableMes, value);
+ }
+ }
+ }
+ }
+
+ private bool _enableCanLog;
+ public bool EnableCanLog
+ {
+ get => _enableCanLog;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("EnableCanLog", value))
+ {
+ Set(ref _enableCanLog, value);
+ }
+ }
+ }
+ }
+
+ private bool _enableDebugMode;
+ public bool EnableDebugMode
+ {
+ get => _enableDebugMode;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("EnableDebugMode", value))
+ {
+ Set(ref _enableDebugMode, value);
+ }
+ }
+ }
+ }
+
+ private bool _enableNGPause;
+ public bool EnableNGPause
+ {
+ get => _enableNGPause;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("EnableNGPause", value))
+ {
+ Set(ref _enableNGPause, value);
+ }
+ }
+ }
+ }
+
+ private bool _enableSimpleMode;
+ public bool EnableSimpleMode
+ {
+ get => _enableSimpleMode;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("EnableSimpleMode", value))
+ {
+ Set(ref _enableSimpleMode, value);
+ foreach (var cell in tsSockets) cell.SetSimpleUI(value);
+ }
+ }
+ }
+ }
+
+ private bool _ui_showInfoDock;
+ public bool UI_ShowInfoDock
+ {
+ get => _ui_showInfoDock;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("UI_ShowInfoDock", value))
+ {
+ Set(ref _ui_showInfoDock, value);
+ detailPanel.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
+ }
+ }
+ }
+ }
+
+ private bool autoExecuteSeq;
+ public bool AutoExecuteSeq
+ {
+ get => autoExecuteSeq;
+ set
+ {
+ Set(ref autoExecuteSeq, value);
+ }
+ }
+
+ private string currentWorkorder;
+ public string CurrentWorkorder
+ {
+ get => currentWorkorder;
+ set
+ {
+ if (WriteIni("UI_CurrentWorkorder", value))
+ {
+ Set(ref currentWorkorder, value);
+ testExecutionWrapper?.SetStationGlobalValue("StationState.WorkOrder", 1, value);
+ testExecutionWrapper?.CommitToDisk(false);
+ }
+ }
+ }
+
+ private bool _DisableScanner;
+ public bool DisableScanner
+ {
+ get => _DisableScanner;
+ set
+ {
+ if (RequirePassword() == true)
+ {
+ if (WriteConfig("DisableScanner", value))
+ {
+ Set(ref _DisableScanner, value);
+ }
+ }
+ }
+ }
+
+ private bool enableManualDebug;
+ public bool EnableManualDebug
+ {
+ get => enableManualDebug;
+ set
+ {
+ if (IsTesting)
+ {
+ MsgBoxHelper2.Warn("璇峰厛缁撴潫鑷姩娴嬭瘯");
+ return;
+ }
+ Set(ref enableManualDebug, value);
+ }
+ }
+
+ public string tsModelPath { get; private set; }
+ private string dataLogPath;
+ private SqliteHelper sqliteHelper;
+
+ private int probeWarn;
+
+ #region 鍥捐〃
+ private SolidColorPaint tooltipTextPaint = new SolidColorPaint(SKColors.Black)
+ {
+ SKTypeface = SKFontManager.Default.MatchCharacter('姹�')
+ };
+
+ public SolidColorPaint TooltipTextPaint
+ {
+ get => tooltipTextPaint;
+ set
+ {
+ Set(ref tooltipTextPaint, value);
+ }
+ }
+
+ public ISeries[] Chart1_series { get; set; } = new PieSeries<double>[]
+ {
+ new PieSeries<double> {
+ Name="Passed",
+ Values = new double[]{ 0},
+ DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer,
+ DataLabelsSize = 15,
+ DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)){
+ SKTypeface = SKFontManager.Default.MatchCharacter('姹�')
+ },
+ Fill = new SolidColorPaint(new SKColor(0, 128, 0)),
+ DataLabelsFormatter =
+ point =>point.StackedValue.Total>0?
+ $"Passed\r\n璁℃暟:{point.Coordinate.PrimaryValue}\r\n鍗犳瘮:{point.StackedValue.Share:P2}":"",
+ ToolTipLabelFormatter = point => $"{point.StackedValue.Share:P2}"
+ },
+ new PieSeries<double> { Values = new double[]{ 0} ,
+ Name="Failed",
+ DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer,
+ DataLabelsSize = 15,
+ DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)){
+ SKTypeface = SKFontManager.Default.MatchCharacter('姹�')
+ },
+ Fill = new SolidColorPaint(new SKColor(255, 0, 0)),
+ DataLabelsFormatter =
+ point =>point.StackedValue.Total>0?
+ $"Failed\r\n璁℃暟:{point.Coordinate.PrimaryValue}\r\n鍗犳瘮:{point.StackedValue.Share:P2}":"",
+ ToolTipLabelFormatter = point => $"{point.StackedValue.Share:P2}"},
+ };
+ #endregion
+
+ public StandardTestUI()
+ {
+ DataContext = this;
+ InitializeComponent();
+
+ InitChart();
+ InitTsEngine();
+ }
+
+ public override void SetBaseFolder(string folder)
+ {
+ CountIniPath = Path.Combine(folder, "Count.ini");
+ base.SetBaseFolder(folder);
+ }
+
+ private void CheckSqlHelper()
+ {
+ string today = DateTime.Now.ToString("yyyyMMdd");
+ string dbPath = Path.Combine(dataLogPath, $"Data_{today}.db");
+ if (!File.Exists(dbPath))
+ {
+ SqliteHelper.CreateDatabase(dbPath);
+ }
+ if (sqliteHelper == null || sqliteHelper.DataBaseFile != dbPath)
+ {
+ sqliteHelper?.Dispose();
+ sqliteHelper = new SqliteHelper(dbPath);
+ sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS serialData (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ sn TEXT NOT NULL,
+ model TEXT,
+ station TEXT NOT NULL,
+ step TEXT NOT NULL,
+ position TEXT,
+ limitDown TEXT,
+ limitUp TEXT,
+ unit TEXT,
+ offset TEXT,
+ value TEXT,
+ status TEXT,
+ finalStatus TEXT,
+ consumeTime TEXT,
+ totalConsumeTime TEXT,
+ time DATETIME NOT NULL);");
+ sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS testResult (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ sn TEXT NOT NULL,
+ model TEXT,
+ station TEXT NOT NULL,
+ position TEXT,
+ status TEXT,
+ result TEXT NOT NULL,
+ time DATETIME NOT NULL);");
+ }
+ }
+
+ public override void InitFinish()
+ {
+ this.ReadCount(out int pass, out int fail);
+ this.DisplayCount(pass, fail);
+ ReadConfig();
+ InitProducts();
+ CheckSqlHelper();
+ base.InitFinish();
+
+ if (ParamSetting.EnableAutoModel)
+ {
+ selectModelPanel.Visibility = Visibility.Collapsed;
+ autoModelPanel.Visibility = Visibility.Visible;
+ CurrentProductModel = string.Empty;
+ }
+
+ try
+ {
+ UserInfo user = UserInfo.User;
+ CurrentUserCode = user?.Code;
+
+ testExecutionWrapper?.SetStationGlobalValue("StationState.OPID", 1, CurrentUserCode);
+ testExecutionWrapper?.CommitToDisk(false);
+ }
+ catch (Exception ex)
+ {
+ log.Warn("Get Login User Failed.", ex);
+ }
+ RefreshUIConfig();
+ }
+
+ /// <summary>
+ /// 鍒锋柊UI閰嶇疆
+ /// </summary>
+ private void RefreshUIConfig()
+ {
+ try
+ {
+ bool enableManualDebug = ParameterManager.GetCommonParameterByName("EnableManualDebug", false);
+ if (enableManualDebug)
+ {
+ Cb_ManualDebug.Visibility = Visibility.Visible;
+ EnableManualDebug = false;
+ }
+ else
+ {
+ Cb_ManualDebug.Visibility = Visibility.Collapsed;
+ EnableManualDebug = false;
+ }
+
+ }
+ catch (Exception ex)
+ {
+ log.Error("RefreshUIConfig Error.", ex);
+ }
+ }
+
+ //private MQConnector mqConnector;
+ //private void InitMQ()
+ //{
+ // mqConnector = new MQConnector("UI");
+ // mqConnector.BeginReceive(HandleMQUI);
+ //}
+ //void HandleMQUI(string msg)
+ //{
+ // string[] arr = msg.Split('|');
+ // int eventCode = int.Parse(arr[0]);
+ // int index = int.Parse(arr[1]);
+ // string strData = arr[2];
+ // Dispatcher.BeginInvoke(new Action(() =>
+ // {
+ // HandleUIMessage(eventCode, index, strData);
+ // }));
+ //}
+
+ /// <summary>
+ /// 鍒濆鍖朩inform鍥捐〃
+ /// </summary>
+ private void InitChart()
+ {
+ // pieChart.Plot.Axes.Frameless();
+ // pieChart.Plot.HideGrid();
+ // pieChart.Plot.DataBackground.Color = ScottPlot.Colors.Transparent;
+
+ // var legend = pieChart.Plot.ShowLegend(Alignment.UpperRight);
+ // legend.Alignment = Alignment.UpperRight;
+ // legend.Padding = new PixelPadding(0);
+ // legend.FontSize = (float)WidthSizeConverter.CountFontSize(20, 300, 300);
+ // pieChart.UserInputProcessor.Disable();
+ // pieChart.Plot.Font.Automatic();
+
+ // List<PieSlice> slices = new List<PieSlice> {
+ //new PieSlice() { Value = 5, FillColor = ScottPlot.Colors.Green,
+ // LabelFontSize = (float)WidthSizeConverter.CountFontSize(20,300,300),
+ // Label = "Pass",LegendText="Pass" },
+ //new PieSlice() { Value = 2, FillColor = ScottPlot.Colors.Red,
+ // LabelFontSize = (float)WidthSizeConverter.CountFontSize(20,300,300),
+ // Label = "Fail",LegendText="Fail" } };
+
+
+ // pie = pieChart.Plot.Add.Pie(slices);
+ }
+
+ private bool? RequirePassword()
+ {
+ if (!IsOptRequirePassword) return true;
+ var win = new RequirePasswordWindow
+ {
+ Owner = Window.GetWindow(this)
+ };
+ return win.ShowDialog();
+ }
+
+ private void InitProducts()
+ {
+ RefershUI();
+ //Products = ProductManager.GetProducts<ObservableCollection<ProductData>>();
+ //if (Products != null && Products.Count > 0)
+ //{
+ // string lastModel = testExecutionWrapper?.GetStationGlobalValue<string>("StationState.ProductModel", 1);
+ // var tempProduct = Products[0];
+ // if (!string.IsNullOrEmpty(lastModel))
+ // {
+ // var product = Products.FirstOrDefault(p => p.Name == lastModel);
+ // if (product != null)
+ // {
+ // tempProduct = product;
+ // }
+ // }
+ // SelectedProduct = tempProduct;
+ //}
+ //RefershUI();
+ //if (tsSockets != null)
+ //{
+ // foreach (SingleControl socket in tsSockets)
+ // {
+ // socket.RefreshProduct(SelectedProduct);
+ // }
+ //}
+ //SampleStatus = false;
+ }
+
+ private bool _isTesting = false;
+ public bool IsTesting
+ {
+ get { return _isTesting; }
+ set
+ {
+ Set(ref _isTesting, value);
+ Dispatcher.Invoke(() =>
+ {
+ BtnStartSeq.IsEnabled = !value;
+ BtnStopSeq.IsEnabled = value;
+ CbProduct.IsEnabled = !value;
+ });
+ }
+ }
+
+ /// <summary>
+ /// 鍒濆鍖朤estStand寮曟搸
+ /// </summary>
+ private void InitTsEngine()
+ {
+ if (ParamSetting.EngineType == "Tap")
+ {
+ testExecutionWrapper = new TapWrapper();
+ }
+ else
+ {
+ testExecutionWrapper = new TeststandWrapper();
+ }
+
+ testExecutionWrapper.EndExecutionEvent += TestExecutionWrapper_EndExecutionEvent;
+ testExecutionWrapper.UIMessageEvent += TestExecutionWrapper_UIMessageEvent;
+ if (testExecutionWrapper is TeststandWrapper tsw)
+ {
+ var AxApplicationMgr = tsw.AxApplicationMgr;
+ ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).BeginInit();
+ host.Child = AxApplicationMgr;
+ ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).EndInit();
+ }
+ try
+ {
+
+ testSeqFile = ParameterManager.GetCommonParameterByName<string>("BurnInMainSeq", string.Empty);
+ }
+ catch
+ {
+
+ }
+ testExecutionWrapper.Init();
+ }
+
+ private void TestExecutionWrapper_UIMessageEvent(object sender, UIMessageEvent e)
+ {
+ try
+ {
+ int eventCode = Convert.ToInt32(e.EventCode);
+ int MethodCode = Convert.ToInt16(e.NumberData);//SocketIndex
+ var data = e.Data;
+ string strData = e.StrData;
+
+ try
+ {
+ log.Debug($"Event: {eventCode}, MethodCode: {MethodCode}, StringData: {strData}");
+ }
+ catch
+ {
+
+ }
+ HandleUIMessage(eventCode, MethodCode, strData, data);
+ }
+ catch (Exception ex)
+ {
+ log.Warn("Ts Event Error.", ex);
+ }
+ }
+
+ private void TestExecutionWrapper_EndExecutionEvent(object sender, ExecutionEndEvent e)
+ {
+ log.Info("AxApplicationMgr_EndExecution");
+ if (ReleaseSequenceFile())
+ {
+ IsTesting = false;
+ log.Info("Release SeqFile Success!");
+ if (canSwitchModel) switchAction?.Invoke();
+ }
+ else
+ {
+ log.Warn("Release SeqFile Fail!");
+ }
+ }
+
+ private volatile bool canSwitchModel = false;
+ private volatile Action switchAction = null;
+
+ private void PluginControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ ShowMask(() =>
+ {
+ try
+ {
+ //Dispatcher.Invoke(() =>
+ //{
+
+ //});
+ isLoadOK = true;
+ if (AutoExecuteSeq) StartSequence();
+ }
+ catch (Exception ex)
+ {
+ log.Error("TsExecution Loaded Failed.", ex);
+ MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.UserControlLoadFailed")}: {ex.Message}");
+ }
+
+ Dispatcher.Invoke(() =>
+ {
+ if (isLoadOK) Loaded -= PluginControl_Loaded;
+ });
+ }, (ex) =>
+ {
+ ToastHelper.Error("Loaded Failed.");
+ log.Error("Loaded Error.", ex);
+ }, null, LangUtil.Get("App.UsercontrolIsLoading"));
+ }
+
+ private void RefershUI()
+ {
+ panel.Children.Clear();
+ string[] names = ParamSetting.SocketNames.Split(',');
+
+ //閲嶆柊鐢熸垚瀛愰潰鏉�
+ for (int i = 0; i < this.socketNum; i++)
+ {
+ string name = string.Empty;
+ if (i < names.Length)
+ {
+ name = names[i];
+ }
+
+ SingleControl control = new SingleControl(i, CountIniPath, this.probeWarn)
+ {
+ SocketName = name
+ };
+ control.parent = this;
+ panel.Children.Add(control);
+ tsSockets.Add(control);
+
+ //createChildInNewThread(i, CountIniPath, ProbeWarn);
+ }
+ if (tsSockets.Count == 1)
+ {
+ panel.Columns = 1;
+ }
+ else if (tsSockets.Count > 0 && (tsSockets.Count % 3) == 0)
+ {
+ panel.Columns = 3;
+ }
+ else
+ {
+ panel.Columns = 2;
+ }
+ }
+
+ /// <summary>
+ /// 璇诲彇Count閰嶇疆鏂囦欢鐨刾ass鍜宖ail鏁�
+ /// </summary>
+ /// <param name="pass">鍚堟牸鏁�</param>
+ /// <param name="fail">涓嶅悎鏍兼暟</param>
+ private void ReadCount(out int pass, out int fail)
+ {
+ pass = Convert.ToInt32(IniFileHelper.ReadIniData("Count", "Pass", "0", CountIniPath));
+ fail = Convert.ToInt32(IniFileHelper.ReadIniData("Count", "Fail", "0", CountIniPath));
+ var fontSize = Convert.ToDouble(IniFileHelper.ReadIniData("Config", "TableFontSize", "12", CountIniPath));
+ this.Resources["tableFontSize"] = 20d;
+ //CbFontSize.SelectedItem = fontSize;
+ }
+
+ private string csvDir;
+
+ /// <summary>
+ /// 璇诲彇閰嶇疆鏂囦欢閲岀殑鍑犱釜灞炴��
+ /// </summary>
+ /// <param name="pass"></param>
+ /// <param name="fail"></param>
+ private void ReadConfig()
+ {
+ HMI.Core.Entity.ParameterData value;
+
+
+ bool flag = false;
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("DefaultEnableMes");
+ if (value != null)
+ {
+ flag = bool.Parse(value.Value);
+ }
+ }
+ catch
+ {
+ flag = false;
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("SocketNum");
+ if (value != null)
+ {
+ this.socketNum = int.Parse(value.Value);
+ }
+ }
+ catch
+ {
+ this.socketNum = 2;
+ }
+
+ if (flag)
+ {
+ EnableMes = true;
+ }
+ else
+ {
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("EnableMes");
+ if (value == null)
+ {
+ CbEnableMes.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _enableMes = bool.Parse(value.Value);
+ Set("EnableMes");
+ }
+ }
+ catch
+ {
+ _enableMes = false;
+ Set("EnableMes");
+ }
+ }
+
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("EnableNGPause");
+ if (value == null)
+ {
+ CbEnableNGPause.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _enableNGPause = bool.Parse(value.Value);
+ Set("EnableNGPause");
+ }
+ }
+ catch
+ {
+ _enableNGPause = false;
+ Set("EnableNGPause");
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("EnableDebugMode");
+ if (value == null)
+ {
+ CbEnableDebugMode.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _enableDebugMode = bool.Parse(value.Value);
+ Set("EnableDebugMode");
+ }
+ }
+ catch
+ {
+ _enableDebugMode = false;
+ Set("EnableDebugMode");
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("DisableScanner");
+ if (value == null)
+ {
+ CbDisableScanner.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ _DisableScanner = bool.Parse(value.Value);
+ Set("DisableScanner");
+ }
+ }
+ catch
+ {
+ _DisableScanner = false;
+ Set("DisableScanner");
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("EnableCanLog");
+ _enableCanLog = bool.Parse(value.Value);
+ Set("EnableCanLog");
+ }
+ catch
+ {
+ _enableCanLog = false;
+ Set("EnableCanLog");
+ }
+
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("EnableSimpleMode");
+ if (value == null)
+ {
+ CbEnableSimpleMode.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ EnableSimpleMode = bool.Parse(value.Value);
+ }
+ }
+ catch
+ {
+ EnableSimpleMode = false;
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("UI_ShowInfoDock");
+ if (value == null)
+ {
+ CbUI_ShowInfoDock.Visibility = Visibility.Collapsed;
+ }
+ else
+ {
+ UI_ShowInfoDock = bool.Parse(value.Value);
+ }
+ }
+ catch
+ {
+ UI_ShowInfoDock = false;
+ }
+
+ try
+ {
+ value = ParameterManager.GetCommonParameterByName("AutoExecuteSeq");
+ AutoExecuteSeq = bool.Parse(value.Value);
+ }
+ catch
+ {
+ AutoExecuteSeq = true;
+ }
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("TestReportFolder");
+ csvDir = value.Value;
+ if (!Directory.Exists(csvDir))
+ {
+ Directory.CreateDirectory(csvDir);
+ }
+ }
+ catch
+ {
+ csvDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestReport");
+ }
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("IsOptRequirePassword");
+ IsOptRequirePassword = bool.Parse(value.Value);
+ }
+ catch
+ {
+ IsOptRequirePassword = false;
+ }
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("UI_CurrentWorkorder");
+ CurrentWorkorder = value.Value;
+ }
+ catch
+ {
+ CurrentWorkorder = string.Empty;
+ }
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("TS_ShowTitleWorkorder");
+ var boo = bool.Parse(value.Value);
+ if (boo)
+ {
+ workorderPanel.Visibility = Visibility.Visible;
+ }
+ }
+ catch
+ {
+ }
+
+ dataLogPath = ParamSetting.DataLogFolder;
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("TsModelPath");
+ tsModelPath = value?.Value ?? @"C:\Users\Public\Documents\National Instruments\TestStand 2020 (64-bit)\Components\Models\ParallelModel.seq";
+ }
+ catch
+ {
+ //@"C:\Program Files\National Instruments\TestStand 2020\Components\Models\TestStandModels\ParallelModel.seq"
+ tsModelPath = @"C:\Users\Public\Documents\National Instruments\TestStand 2020 (64-bit)\Components\Models\ParallelModel.seq";
+ }
+
+ try
+ {
+
+ value = ParameterManager.GetCommonParameterByName("ProbeWarn");
+ probeWarn = int.Parse(value?.Value);
+ }
+ catch
+ {
+ probeWarn = 0;
+ }
+ //Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableMes", "true", CountIniPath));
+ //EnableCanLog = Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableCanLog", "true", CountIniPath));
+ //EnableDebugMode = Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableDebugMode", "true", CountIniPath));
+ }
+
+ private bool WriteConfig(string name, bool value)
+ {
+ try
+ {
+ if (ParameterManager.UpdateCommonParameter(name, value, out string errMsg))
+ {
+ foreach (var ts in tsSockets)
+ {
+ testExecutionWrapper.SetStationGlobalValue($"SystemSetting.{name}", 1, value);
+ if (testExecutionWrapper is TeststandWrapper tsw)
+ {
+ if (name == "EnableDebugMode" && tsw.context?.Sequence != null)
+ {
+ tsw.context.Sequence.FailureAction =
+ value ? FailureActions.FailureAction_None : FailureActions.FailureAction_GotoCleanup;
+ }
+ }
+
+ }
+ return true;
+ }
+ else
+ {
+ throw new Exception(errMsg);
+ }
+ }
+ catch (Exception ex)
+ {
+ MsgBoxHelper2.Error(ex.Message);
+ return false;
+ }
+ }
+
+ private bool WriteIni(string name, object value)
+ {
+ //IniFileHelper.WriteIniData("Config", name, value.ToString(), CountIniPath);
+ try
+ {
+ if (ParameterManager.UpdateCommonParameter(name, value, out string errMsg))
+ {
+ return true;
+ }
+ else
+ {
+ throw new Exception(errMsg);
+ }
+ }
+ catch (Exception ex)
+ {
+ MsgBoxHelper2.Error(ex.Message);
+ return false;
+ }
+ }
+
+ private void WriteCount(double pass, double fail)
+ {
+ IniFileHelper.WriteIniData("Count", "Pass", pass.ToString(), CountIniPath);
+ IniFileHelper.WriteIniData("Count", "Fail", fail.ToString(), CountIniPath);
+ }
+
+ /// <summary>
+ /// 鍒锋柊璁℃暟
+ /// </summary>
+ /// <param name="pass">鍚堟牸鏁�</param>
+ /// <param name="fail">涓嶅悎鏍兼暟</param>
+ private void DisplayCount(double pass, double fail)
+ {
+ double total = pass + fail;
+ double passRate;
+ if (total == 0)
+ {
+ passRate = 0.00;
+ }
+ else
+ {
+ passRate = (double)pass / (double)total * 100.00;
+ }
+ this.TbPass.Text = pass.ToString();
+ this.TbFail.Text = fail.ToString();
+ this.TbTotal.Text = total.ToString();
+ this.TbPassRate.Text = string.Format("{0:0.00}", passRate) + "%";
+
+ Dispatcher.BeginInvoke(new Action(() =>
+ {
+ Chart1_series[0].Values = new double[] { pass };
+ Chart1_series[1].Values = new double[] { fail };
+ // webview2.CoreWebView2?.ExecuteScriptAsync($"drawChart1({res})");
+ }));
+
+ WriteCount(pass, fail);
+ }
+
+ /// <summary>
+ /// 鍚姩鏍蜂欢娴嬭瘯
+ /// </summary>
+ /// <param name="flag"></param>
+ /// <exception cref="Exception"></exception>
+ private void SampleTest(bool flag)
+ {
+ try
+ {
+ testExecutionWrapper.SetStationGlobalValue("StationState.Sample", 1, flag);
+ }
+ catch (Exception ex)
+ {
+ throw new Exception(LangUtil.Get("Ts.SampleTestFail"), ex);
+ }
+ }
+
+ /// <summary>
+ /// 鍚姩娴嬭瘯搴忓垪
+ /// </summary>
+ private void StartSequence()
+ {
+ log.Info($"Start ATE Seq isLoadOK:{isLoadOK}");
+ if (!isLoadOK) return;
+ if (ParamSetting.EnableAutoModel)
+ {
+ try
+ {
+ var path = ParameterManager.GetCommonParameterByName<string>("CommonMainSeq", "");
+ if (string.IsNullOrEmpty(path))
+ {
+ Dispatcher.Invoke(() =>
+ {
+ MsgBoxHelper.Error(this, $"璇峰厛璁剧疆CommonMainSeq鍙傛暟");
+ });
+ return;
+ }
+ this.testSeqFile = path;
+ testExecutionWrapper?.StartSequence(this.testSeqFile, this.tsModelPath);
+ //this.modeSeq = this.engine.GetSequenceFileEx(tsModelPath);
+ //this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
+ //this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
+ IsTesting = true;
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ log.Error("Start Sequence Failed.", ex);
+ MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StartSequenceFail")}: {ex.Message}");
+ });
+ }
+ }
+ else
+ {
+ //MaskWindow mask = new MaskWindow();
+ try
+ {
+ //for (int i = 0; i < tsSockets.Count; i++)
+ //{
+ // testExecutionWrapper?.SetStationGlobalValue($"StationHMI.UUTHMI[{i}].Model", 1, CurrentProductModel);
+ //}
+ //testExecutionWrapper?.CommitToDisk(false);
+ testExecutionWrapper?.StartSequence(this.testSeqFile, this.tsModelPath);
+ IsTesting = true;
+ }
+ catch (Exception ex)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ log.Error("Start Sequence Failed.", ex);
+ MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StartSequenceFail")}: {ex.Message}");
+ });
+ }
+ finally
+ {
+
+ }
+ }
+ }
+
+ public void StopSequenceClick()
+ {
+ canSwitchModel = false;
+ switchAction = null;
+ ShowMask(() =>
+ {
+ bool stoped = StopSequence();
+ }, null, null, LangUtil.Get("App.SequenceIsStopping"));
+ }
+
+ /// <summary>
+ /// 鍋滄娴嬭瘯搴忓垪
+ /// </summary>
+ private bool StopSequence()
+ {
+ try
+ {
+ testExecutionWrapper.SetStationGlobalValue("StationHMI.AllStop", 1, true);
+
+
+ //bool res = this.execution.WaitForEndEx(-1, true, null, null);
+ bool res = this.testExecutionWrapper.WaitForExit();
+ //if (this.execution != null)
+ // Marshal.ReleaseComObject(this.execution);
+ //this.execution = null;
+ return res;
+ }
+ catch (Exception ex)
+ {
+ log.Error("Stop Sequence Failed.", ex);
+ MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StopSequenceFail")}: {ex.Message}");
+ return false;
+ }
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ Button btn = sender as Button;
+ switch (btn.Name)
+ {
+ case "BtnStartSeq":
+ StartClick();
+ break;
+ case "BtnStopSeq":
+ StopSequenceClick();
+ break;
+ case "BtnClearNum":
+ ClearNum();
+ break;
+ case "BtnSample":
+ SampleStatus = !SampleStatus;
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void StartClick()
+ {
+ canSwitchModel = true;
+ ShowMask(() =>
+ {
+ StartSequence();
+ }, null, null, LangUtil.Get("Ts.SequenceIsStarting"));
+ }
+
+ public void UpdatePositionStatus(int index, string status)
+ {
+ this.tsSockets[index].UpdateStatus(status);
+ OnStatusPublished(index, status);
+ this.UpdateTestCounter();
+ }
+
+ private void OnStatusPublished(int index, string status)
+ {
+ if (ParamSetting.CustomerCode != "SOUND")
+ {
+ return;
+ }
+ if (status == "Passed" || status == "Failed")
+ {
+ SaveSOUNDExcel(index, status);
+ }
+ }
+
+ private string ConvertStatus(string status)
+ {
+ if (string.Equals(status, "passed", StringComparison.OrdinalIgnoreCase))
+ {
+ return "Pass";
+ }
+ else
+ {
+ return "Fail";
+ }
+ }
+
+ #region 妗戝痉瀹氬埗
+ private ICell CreateCell(ISheet sheet, int r, int c, ICellStyle style)
+ {
+ var row = sheet.GetRow(r);
+ if (row == null)
+ {
+ row = sheet.CreateRow(r);
+ }
+ var cell = row.GetCell(c);
+ if (cell == null)
+ {
+ cell = row.CreateCell(c);
+ }
+ if (style != null)
+ {
+ cell.CellStyle = style;
+ }
+ return cell;
+ }
+
+ private string GetOutputFolder()
+ {
+ var value = ParameterManager.GetCommonParameterByName("SOUND_Output");
+ if (value != null)
+ {
+ return value.Value;
+ }
+ return ParamSetting.DataLogFolder;
+ }
+
+ private void SaveSOUNDExcel(int index, string status)
+ {
+ lock (this)
+ {
+ try
+ {
+ string outputFolder = GetOutputFolder();
+
+ var socket = this.tsSockets[index];
+ DateTime testTime = DateTime.Now;
+ string filePath = Path.Combine(outputFolder, $"{testTime.ToString("yy-MM-dd")}_{CurrentProductModel}.xls");
+ if (!File.Exists(filePath))
+ {
+ Dictionary<string, int> columnMap = new Dictionary<string, int>();
+ HSSFWorkbook workbook = new HSSFWorkbook();
+ ISheet sheet = workbook.CreateSheet("Sheet1");
+
+ ICellStyle border = workbook.CreateCellStyle();
+ border.BorderBottom = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderTop = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderLeft = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderRight = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BottomBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.TopBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.LeftBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.RightBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+
+
+ ICellStyle red = workbook.CreateCellStyle();
+ red.CloneStyleFrom(border);
+ red.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Red.Index;
+ red.FillPattern = FillPattern.SolidForeground;
+
+ ICellStyle green = workbook.CreateCellStyle();
+ green.CloneStyleFrom(border);
+ green.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.BrightGreen.Index;
+ green.FillPattern = FillPattern.SolidForeground;
+
+ CreateCell(sheet, 1, 0, border);
+ CreateCell(sheet, 1, 1, border);
+ CreateCell(sheet, 1, 2, border);
+ CreateCell(sheet, 1, 3, border);
+ CreateCell(sheet, 1, 4, border);
+
+ CreateCell(sheet, 2, 0, border);
+ CreateCell(sheet, 2, 1, border);
+ CreateCell(sheet, 2, 2, border);
+ CreateCell(sheet, 2, 3, border);
+ CreateCell(sheet, 2, 4, border).SetCellValue("涓婇檺");
+
+ CreateCell(sheet, 3, 0, border).SetCellValue("娴嬭瘯鏃堕棿");
+ CreateCell(sheet, 3, 1, border).SetCellValue("娴嬭瘯浜�");
+ CreateCell(sheet, 3, 2, border).SetCellValue("娴嬭瘯璁惧");
+ CreateCell(sheet, 3, 3, border).SetCellValue("鏉″舰鐮�");
+ CreateCell(sheet, 3, 4, border).SetCellValue("涓嬮檺");
+
+ int column = 5;
+ int row = 4;
+
+ CreateCell(sheet, row, 0, border).SetCellValue(testTime.ToString("*HH:mm:ss*"));
+ CreateCell(sheet, row, 1, border).SetCellValue(UserInfo.User.Name);
+ CreateCell(sheet, row, 2, border).SetCellValue(ParamSetting.StationName);
+ CreateCell(sheet, row, 3, border).SetCellValue(socket.Barcode);
+ CreateCell(sheet, row, 4, status == "Passed" ? green : red).SetCellValue(ConvertStatus(status));
+
+ foreach (var data in socket.Datasource)
+ {
+ int col = column;
+ if (columnMap.ContainsKey(data.Name))
+ {
+ col = columnMap[data.Name];
+ }
+ else
+ {
+ columnMap[data.Name] = column++;
+ }
+
+ CreateCell(sheet, 1, col, border).SetCellValue(data.Name);
+ CreateCell(sheet, 2, col, border).SetCellValue(data.LimitUp);
+ CreateCell(sheet, 3, col, border).SetCellValue(data.LimitDown);
+ CreateCell(sheet, row, col, data.Result == "Failed" ? red : border).SetCellValue(data.Value);
+ }
+ using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate))
+ {
+ workbook.Write(fs);
+ }
+ }
+ else
+ {
+ using (FileStream fs = File.OpenRead(filePath))
+ {
+ IWorkbook workbook = new HSSFWorkbook(fs);
+ ICellStyle border = workbook.CreateCellStyle();
+ border.BorderBottom = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderTop = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderLeft = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BorderRight = NPOI.SS.UserModel.BorderStyle.Thin;
+ border.BottomBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.TopBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.LeftBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+ border.RightBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index;
+
+
+ ICellStyle red = workbook.CreateCellStyle();
+ red.CloneStyleFrom(border);
+ red.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Red.Index;
+ red.FillPattern = FillPattern.SolidForeground;
+
+ ICellStyle green = workbook.CreateCellStyle();
+ green.CloneStyleFrom(border);
+ green.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.BrightGreen.Index;
+ green.FillPattern = FillPattern.SolidForeground;
+
+ ISheet sheet = workbook.GetSheet("Sheet1");
+ Dictionary<string, int> columnMap = new Dictionary<string, int>();
+ var titleRow = sheet.GetRow(1);
+ for (int i = 0; i < titleRow.LastCellNum; i++)
+ {
+ if (!string.IsNullOrEmpty(titleRow.GetCell(i).StringCellValue))
+ {
+ columnMap[titleRow.GetCell(i).StringCellValue] = i;
+ }
+ }
+ int row = sheet.LastRowNum + 1;
+ CreateCell(sheet, row, 0, border).SetCellValue(testTime.ToString("*HH:mm:ss*"));
+ CreateCell(sheet, row, 1, border).SetCellValue(UserInfo.User.Name);
+ CreateCell(sheet, row, 2, border).SetCellValue(ParamSetting.StationName);
+ CreateCell(sheet, row, 3, border).SetCellValue(socket.Barcode);
+ CreateCell(sheet, row, 4, status == "Passed" ? green : red).SetCellValue(ConvertStatus(status));
+
+ foreach (var data in socket.Datasource)
+ {
+ int col = titleRow.LastCellNum;
+ if (columnMap.ContainsKey(data.Name))
+ {
+ col = columnMap[data.Name];
+ }
+ else
+ {
+ columnMap[data.Name] = col;
+ }
+
+ CreateCell(sheet, 1, col, border).SetCellValue(data.Name);
+ CreateCell(sheet, 2, col, border).SetCellValue(data.LimitUp);
+ CreateCell(sheet, 3, col, border).SetCellValue(data.LimitDown);
+ CreateCell(sheet, row, col, data.Result == "Failed" ? red : border).SetCellValue(data.Value);
+ }
+ using (FileStream nfs = new FileStream(filePath, FileMode.OpenOrCreate))
+ {
+ workbook.Write(nfs);
+ }
+ }
+ }
+
+ }
+ catch (Exception ex)
+ {
+ log.Error("SOUND Export Excel Data Error.", ex);
+ }
+ }
+ }
+ #endregion
+
+ public void UpdatePositionProcessValue(int index, int value)
+ {
+ this.tsSockets[index].SetTestProcess(value);
+ }
+
+ public void PausePosition(int index)
+ {
+ this.tsSockets[index].ShowPausePanel();
+ }
+
+ public void AddTestData(int index, string data)
+ {
+ string[] TestItems = data.Split(',');
+ tsSockets[index].AddTestData(TestItems[0], TestItems[1], TestItems[2], TestItems[3],
+ TestItems[4], TestItems[5], TestItems[6]);
+ }
+
+ public void SetPositionBarcode(int index, string strData)
+ {
+ if (strData.Contains(";"))
+ {
+ var strs = strData.Split(';');
+ this.tsSockets[index].Barcode = strs[0];
+ this.tsSockets[index].Model = strs[1];
+ }
+ else
+ {
+ this.tsSockets[index].Barcode = strData;
+ }
+ }
+
+ public void SavePositionCSV(int index, string csvPath)
+ {
+ //var time = DateTime.Now;
+ //string status = this.tsSockets[index].Status == "Passed" ? "PASS" : "FAIL";
+ //string csvPath = Path.Combine(csvDir, SelectedProduct?.Name ?? "鏈煡浜у搧", time.ToString("yyyyMMdd"), status,
+ // $"{this.tsSockets[index].Barcode}-{time.ToString("HHmmss")}.csv");
+ this.tsSockets[index].WriteToCSV(csvPath);
+ }
+
+ public void SaveTemplateCSV(int index)
+ {
+ var time = DateTime.Now;
+ string status = this.tsSockets[index].Status == "Passed" ? "PASS" : "FAIL";
+ string csvPath = Path.Combine(csvDir, CurrentProductModel ?? "鏈煡浜у搧", time.ToString("yyyyMMdd"), status, $"{this.tsSockets[index].Barcode}-{time.ToString("HHmmss")}.csv");
+ this.tsSockets[index].WriteToCSV(csvPath);
+ }
+
+ public void SavePositionDB(int index, string status)
+ {
+ CheckSqlHelper();
+ var socket = this.tsSockets[index];
+ var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ string snr = socket.Barcode;
+ string model = CurrentProductModel;
+
+ List<string> sqls = new List<string>();
+ foreach (var data in socket.Datasource)
+ {
+ sqls.Add($@"INSERT INTO serialData (sn,model,station,step,position,limitDown,limitUp,unit,value,status,finalStatus,consumeTime,totalConsumeTime,time) VALUES
+('{snr}','{model}','','{data.Name}','{index}','{data.LimitDown}','{data.LimitUp}','{data.Unit}','{data.Value}','{data.Result}','{socket.Status}','{data.ConsumeTime}','{socket.ElapseTime}','{time}')");
+ }
+ sqliteHelper.ExecuteBatchSql(sqls);
+
+ var ssql = $@"INSERT INTO testResult (sn,model,station,result,time,position,status) VALUES
+('{snr}','{model}','','{socket.Status}','{time}','{index}','{status}')";
+ sqliteHelper.ExecuteSql(ssql);
+ }
+
+ private void HandleUIMessage(int eventCode, int MethodCode, string strData, object xData)
+ {
+ if (eventCode == 10035)
+ {
+ string[] arr_10035 = strData.Split(';');
+
+ int[][] points = new int[arr_10035.Length - 1][];
+
+ for (int i = 1; i < arr_10035.Length; i++)
+ {
+ string[] strs = arr_10035[i].Split(',');
+ points[i - 1] = new int[2] { int.Parse(strs[0]), int.Parse(strs[1]) };
+ }
+ this.tsSockets[MethodCode].DrawScreenPoints(int.Parse(arr_10035[0]), points);
+ return;
+
+ }
+ else if (eventCode == 100350)
+ {
+ string[] arr_100350 = strData.Split(';');
+
+ int[][] points_0 = new int[arr_100350.Length - 1][];
+
+ for (int i = 1; i < arr_100350.Length; i++)
+ {
+ if (string.IsNullOrEmpty(arr_100350[i]))
+ {
+ continue;
+ }
+ string[] strs = arr_100350[i].Split(',');
+ points_0[i - 1] = new int[2] { int.Parse(strs[0]), int.Parse(strs[1]) };
+ }
+ this.tsSockets[MethodCode].DrawNewScreenPoints(int.Parse(arr_100350[0]), points_0);
+ return;
+ }
+ else if (eventCode == 10036)
+ {
+ this.tsSockets[MethodCode].ClearScreenPoint(int.Parse(strData));
+ return;
+ }
+ else if (eventCode == 10036)
+ {
+ this.tsSockets[MethodCode].ClearScreenPoint(int.Parse(strData));
+ return;
+ }
+ if (MethodCode == 0)
+ {
+ /// 璁剧疆鍙ユ焺
+ testExecutionWrapper?.SetContext((SequenceContext)xData);
+ return;
+ }
+ if (MethodCode == 3)
+ {
+ /// 鏇存柊鐐夊瓙鏃堕棿淇℃伅
+ string[] datas_10014 = strData.Split(';');
+ int ovenIndex = int.Parse(datas_10014[0]);
+ string startTime = datas_10014[1];
+ string consumeTime = datas_10014[2];
+ string planEndTime = datas_10014[3];
+ tsSockets[ovenIndex].ElapseTime = consumeTime;
+ return;
+ }
+ else if (MethodCode == 4)
+ {
+ /// 淇敼宸ヤ綅搴曢儴淇℃伅鏍�
+ string[] datas_10014 = strData.Split(';');
+ int ovenIndex = int.Parse(datas_10014[0]);
+ string statusBar = datas_10014[1];
+ this.tsSockets[ovenIndex].CurrentStep = statusBar;
+ return;
+ }
+ else if (MethodCode == 6)
+ {
+ Dispatcher.Invoke(() =>
+ {/// 鏇存柊绌翠綅鐘舵��
+ string[] datas_10015 = strData.Split(';');
+ int ovenIndex = int.Parse(datas_10015[0]);
+ int cellIndex = int.Parse(datas_10015[1]);
+ string status_10015 = datas_10015[2];
+ UpdatePositionStatus(ovenIndex, status_10015);
+ });
+ return;
+ }
+ else if (MethodCode == 9)
+ {
+ if (string.Equals(strData, "stop", StringComparison.OrdinalIgnoreCase))
+ {
+ IsTesting = false;
+ }
+ return;
+ }
+ else if (MethodCode == 11)
+ {
+ // 娓呯┖娴嬭瘯鏁版嵁
+ string[] datas_10015 = strData.Split(';');
+ int ovenIndex = int.Parse(datas_10015[0]);
+ this.tsSockets[ovenIndex].IniTestData();
+ return;
+ }
+ else if (MethodCode == 18)
+ {
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ string filePath = datas[2];
+
+ if (ParamSetting.CustomerCode == "SOUND")
+ {
+ this.tsSockets[ovenIndex].WriteToExcel(filePath);
+ Dispatcher.Invoke(() =>
+ {
+ SavePositionDB(ovenIndex, tsSockets[ovenIndex].Status);
+ });
+ return;
+ }
+ try
+ {
+ SavePositionCSV(ovenIndex, filePath);
+ Dispatcher.Invoke(() =>
+ {
+ var sss = this.tsSockets[ovenIndex].Status;
+ SavePositionDB(ovenIndex, sss);
+ });
+ }
+ catch (Exception ex)
+ {
+ log.Warn("鍐機SV/DB鏁版嵁澶辫触.", ex);
+ SaveTemplateCSV(ovenIndex);
+ }
+ return;
+ }
+ else if (MethodCode == 9000)
+ {
+ //鏇存柊鍨嬪彿
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ CurrentProductModel = datas[2];
+ return;
+ }
+ else if (MethodCode == 9001)
+ {
+ //鏇存柊鏉$爜
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ SetPositionBarcode(ovenIndex, datas[2]);
+ return;
+ }
+ else if (MethodCode == 9002)
+ {
+ //鏄剧ず娴嬭瘯杩涘害
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ int ProcessVal = Convert.ToInt32(datas[2]);
+ this.tsSockets[ovenIndex].SetTestProcess(ProcessVal);
+ return;
+ }
+ else if (MethodCode == 9003)
+ {
+ //寮�濮嬭鏃�
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ tsSockets[ovenIndex].ElapseTime = "Start Timing";
+ return;
+ }
+ else if (MethodCode == 9004)
+ {
+ //缁撴潫璁℃椂
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ tsSockets[ovenIndex].ElapseTime = "Stop Timing";
+ return;
+ }
+ else if (MethodCode == 9005)
+ {
+ //鎵嬪姩璁剧疆鑰楁椂
+ string[] datas = strData.Split(';');
+ int ovenIndex = int.Parse(datas[0]);
+ int cellIndex = int.Parse(datas[1]);
+ tsSockets[ovenIndex].ElapseTime = datas[2];
+ return;
+ }
+ }
+
+ private void SwitchSequenceByModel(int index, string model, string snr)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ var target = Products.FirstOrDefault(e => e.Name == model);
+
+ ShowMask(() =>
+ {
+ bool stoped = StopSequence();
+ //if (stoped)
+ //{
+ // IsTesting = false;
+ //}
+ }, null, null, "鎹㈠瀷涓紝璇风◢鍚�...");
+
+ if (target == null)
+ {
+ MsgBoxHelper2.Warn($"璇峰厛鍦ㄤ骇鍝佺鐞嗛〉闈㈤厤缃瀷鍙� [{model}] 鐨勪骇鍝佷俊鎭紒");
+ return;
+ }
+
+ switchAction = () =>
+ {
+ switchAction = null;
+ log?.Info($"灏濊瘯鑷姩鎹㈠瀷: {switchAction}");
+ if (canSwitchModel)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ try
+ {
+ log?.Info("寮�濮嬭嚜鍔ㄦ崲鍨�");
+ SelectedProduct = target;
+ testExecutionWrapper?.SetStationGlobalValue($"CacheSNR[{index}]", 1, snr);
+ testExecutionWrapper?.CommitToDisk(false);
+ StartClick();
+ }
+ catch (Exception ex)
+ {
+ log?.Error("鑷姩鎹㈠瀷澶辫触", ex);
+ }
+ });
+ }
+ };
+ });
+ }
+
+ private void ShowAlarm(AlarmText alarm)
+ {
+ AlarmShowHelper.ShowAlarm(alarm);
+ mSMQHelper.Send("Alarm", "PC", $"ATE娴嬭瘯 {alarm.AlarmInfo}", 2);
+ }
+
+ private void UpdateTestCounter()
+ {
+ double PassNum = 0;
+ double FailNum = 0;
+ foreach (SingleControl socket in this.tsSockets)
+ {
+ PassNum += socket.PassNum;
+ FailNum += socket.FailNum;
+ }
+ this.DisplayCount(PassNum, FailNum);
+ }
+
+ private void SetTestSeqFile(ProductData product)
+ {
+ if (product == null)
+ {
+ testSeqFile = null;
+ return;
+ }
+ string name = product.Name;
+ List<HMI.Core.Entity.ParameterData> parameters = ProductManager.GetParametersByProductName(name);
+ testSeqFile = parameters.FirstOrDefault(p => p.Name == "TestSequence")?.Value?.ToString();
+ if (AutoExecuteSeq)
+ {
+ StartSequence();
+ }
+ }
+
+ private void ClearNum()
+ {
+ if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), LangUtil.Get("Ts.MakeSureClearNum")) == MessageBoxResult.OK)
+ {
+ PassAmount = 0;
+ FailAmount = 0;
+ //this.OpcHandle.DoFctWriteSingle("Count_Clear", "1", out string ErrMsg);
+ foreach (SingleControl socket in tsSockets)
+ {
+ socket.ResetCounter();
+ }
+ DisplayCount(PassAmount, FailAmount);
+ }
+ }
+
+ public override void OnClose(object sender, CancelEventArgs e)
+ {
+ try
+ {
+ StopSequence();
+ base.OnClose(sender, e);
+ }
+ catch (Exception ex)
+ {
+ log.Error("OnClose Error.", ex);
+ }
+ }
+
+ public override void DoClose(object sender, EventArgs e)
+ {
+ testExecutionWrapper.Dispose();
+ base.DoClose(sender, e);
+ }
+
+ /// <summary>
+ /// 閲婃斁鎵�鏈夌殑搴忓垪鏂囦欢
+ /// </summary>
+ private bool ReleaseSequenceFile()
+ {
+ try
+ {
+ lock (this)
+ {
+ //execution.Abort();
+ int count = 10;
+ while (count > 0)
+ {
+ try
+ {
+ return testExecutionWrapper.ReleaseSeqFile();
+ }
+ catch (Exception ex)
+ {
+ log.Warn($"ReleaseSequenceFile In {count} Times Fail.", ex);
+ count--;
+ System.Threading.Thread.Sleep(1000);
+ }
+ }
+ log.Warn($"ReleaseSequenceFile Failed.");
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ log.Warn("ReleaseSequenceFile Error.", ex);
+ return false;
+ }
+ }
+
+ private void PluginControl_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+
+ }
+
+ private void CbFontSize_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ try
+ {
+ if (e.AddedItems != null && e.AddedItems.Count > 0)
+ {
+ var fontSize = (double)e.AddedItems[0];
+ this.Resources["tableFontSize"] = fontSize;
+ IniFileHelper.WriteIniData("Config", "TableFontSize", fontSize.ToString(), CountIniPath);
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private void Label_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ //workOrderEditor.Focus();
+ //workOrderEditor.IsEditing = true;
+ if (RequirePassword() == true)
+ {
+ var win = new WorkOrderWin
+ {
+ WorkOrder = CurrentWorkorder,
+ Owner = Window.GetWindow(this)
+ };
+ if (win.ShowDialog() == true)
+ {
+ CurrentWorkorder = win.WorkOrder;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Plugin.StandardTest/WorkOrderWin.xaml b/Plugin.StandardTest/WorkOrderWin.xaml
new file mode 100644
index 0000000..7cc6410
--- /dev/null
+++ b/Plugin.StandardTest/WorkOrderWin.xaml
@@ -0,0 +1,24 @@
+锘�<pu:WindowX x:Class="Plugin.StandardTest.WorkOrderWin"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:Plugin.StandardTest"
+ mc:Ignorable="d"
+ Style="{StaticResource ModalWindow}"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
+ SizeToContent="Height"
+ FontSize="14"
+ WindowStartupLocation="CenterOwner"
+ pu:WindowXModalDialog.OKButtonContent="纭畾"
+ Title="璇疯緭鍏ュ伐鍗曞彿" Width="500">
+ <Grid dp:GridHelper.Space="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
+ <pu:FormGroup Header="宸ュ崟鍙�" Orientation="Horizontal" Style="{StaticResource RequiredInputFormGroupStyle}">
+ <TextBox Text="{Binding WorkOrder}" Style="{StaticResource EditorTextBoxStyle}"/>
+ </pu:FormGroup>
+ </Grid>
+</pu:WindowX>
diff --git a/Plugin.StandardTest/WorkOrderWin.xaml.cs b/Plugin.StandardTest/WorkOrderWin.xaml.cs
new file mode 100644
index 0000000..92e71f1
--- /dev/null
+++ b/Plugin.StandardTest/WorkOrderWin.xaml.cs
@@ -0,0 +1,36 @@
+锘縰sing Panuon.WPF.UI;
+using System.ComponentModel;
+
+namespace Plugin.StandardTest
+{
+ /// <summary>
+ /// RequirePasswordWindow.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class WorkOrderWin : WindowX
+ {
+ public WorkOrderWin()
+ {
+ this.DataContext = this;
+ InitializeComponent();
+ }
+
+ private string workOrder;
+ public string WorkOrder
+ {
+ get => workOrder;
+ set
+ {
+ Set(ref workOrder, value);
+ }
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ //Press Continue Button
+ if (DialogResult == true)
+ {
+ }
+ base.OnClosing(e);
+ }
+ }
+}
diff --git a/Plugin.TestPlanEditor/AssemblyInfo.cs b/Plugin.TestPlanEditor/AssemblyInfo.cs
new file mode 100644
index 0000000..aca5236
--- /dev/null
+++ b/Plugin.TestPlanEditor/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+using HMI.Core.Annotation;
+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)
+)]
+[assembly:PluginClassName("Plugin.TestPlanEditor.EditorUI")]
\ No newline at end of file
diff --git a/Plugin.TestPlanEditor/EditorUI.xaml b/Plugin.TestPlanEditor/EditorUI.xaml
new file mode 100644
index 0000000..65e21cd
--- /dev/null
+++ b/Plugin.TestPlanEditor/EditorUI.xaml
@@ -0,0 +1,81 @@
+锘�<hb:SimpleMaskControl x:Class="Plugin.TestPlanEditor.EditorUI"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Plugin.TestPlanEditor"
+ mc:Ignorable="d"
+ xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base"
+ xmlns:ote="clr-namespace:OpenTapEditor;assembly=SeqEditor"
+ xmlns:ui="clr-namespace:UILib;assembly=UILib"
+ xmlns:op="clr-namespace:OpenTap;assembly=OpenTap"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/UILib;component/Styles.xaml"/>
+ <ResourceDictionary Source="/SeqEditor;component/UIStyle.xaml"/>
+ <pu:StyleDictionary Excludes="Menu,ListView,ToggleButton,ContextMenu,TabControl,CheckBox,TextBox,Button" />
+ </ResourceDictionary.MergedDictionaries>
+
+ <sys:Double x:Key="GlobalFontSize">14</sys:Double>
+ <ui:ReverseBoolConverter x:Key="ReverseBool"/>
+ <ObjectDataProvider x:Key="BreakConditionValues"
+ MethodName="GetValues"
+ ObjectType="{x:Type sys:Enum}">
+ <ObjectDataProvider.MethodParameters>
+ <x:Type TypeName="op:BreakCondition"/>
+ </ObjectDataProvider.MethodParameters>
+ </ObjectDataProvider>
+
+ <sys:Double x:Key="{x:Static SystemParameters.VerticalScrollBarWidthKey}">3</sys:Double>
+ <sys:Double x:Key="{x:Static SystemParameters.HorizontalScrollBarHeightKey}">3</sys:Double>
+
+ <Style TargetType="ScrollViewer">
+ <Style.Resources>
+ <Style TargetType="ScrollBar" BasedOn="{StaticResource ScrollStyle_Mobile}"/>
+ </Style.Resources>
+ </Style>
+
+ <Style TargetType="TabItem">
+ <Setter Property="Padding" Value="5"/>
+ <Setter Property="Background" Value="#f2f3f5"/>
+ <Setter Property="BorderThickness" Value="0"/>
+ <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
+ <Setter Property="FontSize" Value="10"/>
+ <Setter Property="Cursor" Value="Hand"/>
+ <Setter Property="Template">
+ <Setter.Value>
+ <ControlTemplate TargetType="TabItem">
+ <Border x:Name="border" Background="{TemplateBinding Background}"
+BorderBrush="{TemplateBinding BorderBrush}"
+BorderThickness="{TemplateBinding BorderThickness}"
+Padding="{TemplateBinding Padding}">
+ <ContentPresenter x:Name="contentPresenter"
+ ContentSource="Header"
+ HorizontalAlignment="Center"
+ VerticalAlignment="Center"/>
+ </Border>
+ <ControlTemplate.Triggers>
+ <Trigger Property="IsMouseOver" Value="True">
+ <Setter TargetName="border" Property="Background" Value="#e0e9f2"/>
+ </Trigger>
+ <Trigger Property="IsSelected" Value="True">
+ <Setter TargetName="border" Property="Background" Value="#bcddee"/>
+ <Setter TargetName="border" Property="BorderThickness" Value="0,0,0,2"/>
+ <Setter TargetName="border" Property="BorderBrush" Value="#3498DB"/>
+ </Trigger>
+ </ControlTemplate.Triggers>
+ </ControlTemplate>
+ </Setter.Value>
+ </Setter>
+ </Style>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid>
+ <ote:SeqEditorUI x:Name="editor"/>
+ </Grid>
+</hb:SimpleMaskControl>
diff --git a/Plugin.TestPlanEditor/EditorUI.xaml.cs b/Plugin.TestPlanEditor/EditorUI.xaml.cs
new file mode 100644
index 0000000..b8f0ca6
--- /dev/null
+++ b/Plugin.TestPlanEditor/EditorUI.xaml.cs
@@ -0,0 +1,29 @@
+锘縰sing AddInPlugin;
+using HMI.Base;
+
+namespace Plugin.TestPlanEditor
+{
+ /// <summary>
+ /// EditorUI.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class EditorUI : SimpleMaskControl
+ {
+ public EditorUI()
+ {
+ InitializeComponent();
+ }
+
+ public override void InitFinish()
+ {
+ try
+ {
+ LvlibpLoader.PreLoad();
+ log.Debug("PreLoad completed.");
+ }
+ catch (Exception ex)
+ {
+ log.Error("Labview PreLoad error.", ex);
+ }
+ }
+ }
+}
diff --git a/Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj b/Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj
new file mode 100644
index 0000000..bc639cd
--- /dev/null
+++ b/Plugin.TestPlanEditor/Plugin.TestPlanEditor.csproj
@@ -0,0 +1,33 @@
+锘�<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <TargetFramework>net472</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <UseWPF>true</UseWPF>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ <PackageReference Include="log4net" Version="3.0.4" />
+ <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\VisualStudio\UILib\UILib.csproj" />
+ <ProjectReference Include="..\..\..\VisualStudio\UtilLib\UtilLib.csproj" />
+ <ProjectReference Include="..\..\OpenTapEditor\Editor\SeqEditor.csproj" />
+ <ProjectReference Include="..\..\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj" />
+ <ProjectReference Include="..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj" />
+ <ProjectReference Include="..\..\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj" />
+ <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" />
+ <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" />
+ <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
+ <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
+ <ProjectReference Include="..\SimpleUtil\HMI.LogUtil\HMI.LogUtil.csproj" />
+ <ProjectReference Include="..\SimpleUtil\HMI.Util.Base\HMI.Util.Base.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/Plugin.TestResult/ManualDebugWindow.xaml b/Plugin.TestResult/ManualDebugWindow.xaml
index 9313b73..07d4177 100644
--- a/Plugin.TestResult/ManualDebugWindow.xaml
+++ b/Plugin.TestResult/ManualDebugWindow.xaml
@@ -4,7 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
- xmlns:sb="clr-namespace:Util.SB;assembly=Util.ManualDebug"
+ xmlns:sb="clr-namespace:Util.ManualDebug;assembly=Util.ManualDebug"
Title="ManualDebugWindow"
xmlns:pu="https://opensource.panuon.com/wpf-ui"
Style="{StaticResource ModalWindow}"
@@ -13,7 +13,7 @@
pu:WindowXModalDialog.Buttons="{x:Null}"
Height="1000" Width="900">
<Grid x:Name="sb">
- <sb:ManualDebug OvenName="{Binding OvenName,Mode=OneWay}"
+ <sb:OldManualDebug OvenName="{Binding OvenName,Mode=OneWay}"
CellName="{Binding CellName,Mode=OneWay}"
OvenIndex="{Binding OvenIndex,Mode=OneWay}"
CellIndex="{Binding CellIndex,Mode=OneWay}"/>
diff --git a/Plugin.TestResult/ManualDebugWindow.xaml.cs b/Plugin.TestResult/ManualDebugWindow.xaml.cs
index c40668b..68f8c3d 100644
--- a/Plugin.TestResult/ManualDebugWindow.xaml.cs
+++ b/Plugin.TestResult/ManualDebugWindow.xaml.cs
@@ -5,7 +5,7 @@
using System.ComponentModel;
using System.IO;
using System.Windows;
-using Util.SB;
+using Util.ManualDebug;
namespace Plugin.TestResult
{
@@ -15,13 +15,13 @@
public partial class ManualDebugWindow : WindowX
{
- private ManualDebug debug;
+ private OldManualDebug debug;
public ManualDebugWindow(string seqPath, string modelPath)
{
DataContext = this;
InitializeComponent();
- debug = (ManualDebug)sb.Children[0];
+ debug = (OldManualDebug)sb.Children[0];
ReadBtns();
//List<BtnConfig> settings = new List<BtnConfig> { new BtnConfig { Title = "涓婄數婵�娲�", Name = "Active" } };
debug.seqPath = seqPath;
diff --git a/Plugin.TestResult/Plugin.TestResult.csproj b/Plugin.TestResult/Plugin.TestResult.csproj
index dd34f6b..7cff614 100644
--- a/Plugin.TestResult/Plugin.TestResult.csproj
+++ b/Plugin.TestResult/Plugin.TestResult.csproj
@@ -360,6 +360,10 @@
<Resource Include="Resource\barcode.png" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj">
+ <Project>{efbd516d-3dc9-9d75-f082-48fbc76715f7}</Project>
+ <Name>Tap.Engine</Name>
+ </ProjectReference>
<ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
<Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
<Name>HMI.Base</Name>
diff --git a/Plugin.TestResult/TestResultView.xaml.cs b/Plugin.TestResult/TestResultView.xaml.cs
index 0d461da..a207482 100644
--- a/Plugin.TestResult/TestResultView.xaml.cs
+++ b/Plugin.TestResult/TestResultView.xaml.cs
@@ -218,11 +218,11 @@
}
else
{
- executionWrapper = new TsExecutionWrapper();
+ executionWrapper = new TeststandWrapper();
}
executionWrapper.UIMessageEvent += ExecutionWrapper_UIMessageEvent;
- if (executionWrapper is TsExecutionWrapper tsWrapper)
+ if (executionWrapper is TeststandWrapper tsWrapper)
{
((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).BeginInit();
host.Child = tsWrapper.AxApplicationMgr;
@@ -615,7 +615,7 @@
public void ReleaseResource()
{
executionWrapper.UIMessageEvent -= ExecutionWrapper_UIMessageEvent;
-
+ executionWrapper.Dispose();
foreach (var table in tables)
{
table.RelaseResource();
diff --git a/Plugin.TsExecution/Plugin.TsExecution.csproj b/Plugin.TsExecution/Plugin.TsExecution.csproj
index e63a2e4..59d1d67 100644
--- a/Plugin.TsExecution/Plugin.TsExecution.csproj
+++ b/Plugin.TsExecution/Plugin.TsExecution.csproj
@@ -240,6 +240,10 @@
</None>
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj">
+ <Project>{efbd516d-3dc9-9d75-f082-48fbc76715f7}</Project>
+ <Name>Tap.Engine</Name>
+ </ProjectReference>
<ProjectReference Include="..\HMI.Base\HMI.Base.csproj">
<Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project>
<Name>HMI.Base</Name>
@@ -272,6 +276,10 @@
<Project>{CF5BEBE9-2803-49C3-B773-73D082E0984A}</Project>
<Name>HMI.Simple.Util</Name>
</ProjectReference>
+ <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj">
+ <Project>{d0944e62-ee52-4abe-832a-a09d98814db1}</Project>
+ <Name>Util.ManualDebug</Name>
+ </ProjectReference>
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Connected Services\" />
diff --git a/Plugin.TsExecution/Properties/AssemblyInfo.cs b/Plugin.TsExecution/Properties/AssemblyInfo.cs
index 87940bc..155cbbe 100644
--- a/Plugin.TsExecution/Properties/AssemblyInfo.cs
+++ b/Plugin.TsExecution/Properties/AssemblyInfo.cs
@@ -49,6 +49,6 @@
// 鐢熸垚鍙�
// 淇鍙�
//
-[assembly: AssemblyVersion("1.0.0.7")]
-[assembly: AssemblyFileVersion("1.0.0.7")]
+[assembly: AssemblyVersion("1.0.0.8")]
+[assembly: AssemblyFileVersion("1.0.0.8")]
[assembly: PluginClassName("Plugin.TsExecution.TsExecution")]
diff --git a/Plugin.TsExecution/SingleControl.xaml b/Plugin.TsExecution/SingleControl.xaml
index b40655c..4bbacd5 100644
--- a/Plugin.TsExecution/SingleControl.xaml
+++ b/Plugin.TsExecution/SingleControl.xaml
@@ -12,6 +12,7 @@
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Loaded="UserControl_Loaded"
Background="#ccc6c7"
+ PreviewMouseRightButtonDown="control_PreviewMouseRightButtonDown"
FontSize="{Binding ActualHeight,RelativeSource={RelativeSource AncestorType=UserControl},
Converter={StaticResource hfc}, ConverterParameter='12,816'}"
x:Name="control"
diff --git a/Plugin.TsExecution/SingleControl.xaml.cs b/Plugin.TsExecution/SingleControl.xaml.cs
index 35ceb3b..0c3ecb0 100644
--- a/Plugin.TsExecution/SingleControl.xaml.cs
+++ b/Plugin.TsExecution/SingleControl.xaml.cs
@@ -2,11 +2,9 @@
using HMI.Core.Manager;
using HMI.Core.Setting;
using HMI.Core.Util;
+using HMI.UI.Util;
using HMI.Util;
using NationalInstruments.TestStand.Interop.API;
-using NPOI.HSSF.UserModel;
-using NPOI.SS.Formula.Functions;
-using NPOI.SS.UserModel;
using Plugin.TsExecution.Entity;
using System;
using System.Collections.Generic;
@@ -17,6 +15,7 @@
using System.Timers;
using System.Windows;
using System.Windows.Controls;
+using Util.ManualDebug;
namespace Plugin.TsExecution
{
@@ -65,6 +64,8 @@
Set(ref totalNum, value);
}
}
+
+ public TsExecution parent { get; set; }
private int passNum;
public int PassNum
@@ -271,7 +272,7 @@
try
{
var value = ParameterManager.GetCommonParameterByName("AutoTestMode");
- IsAutoMode = bool.Parse(value.Value);
+ IsAutoMode = value == null ? true : bool.Parse(value.Value);
}
catch
{
@@ -373,7 +374,10 @@
public void IniTestData()
{
- Datasource.Clear();
+ Dispatcher.Invoke(() =>
+ {
+ Datasource.Clear();
+ });
}
/// <summary>
@@ -564,7 +568,7 @@
private void SendSnr2Ts()
{
-
+
}
private void pausePanel_Loaded(object sender, RoutedEventArgs e)
@@ -735,5 +739,57 @@
}.ShowDialog();
}
}
+
+ private void control_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (parent.EnableManualDebug)
+ {
+ ContextMenu contextMenu = new ContextMenu();
+ contextMenu.Closed += ContextMenu_Closed;
+ var item = new MenuItem
+ {
+ IsCheckable = false,
+ Name = "manualDebugItem",
+ Header = this.lang("鎵嬪姩璋冭瘯"),
+ };
+ item.Click += Item_Click; ;
+ contextMenu.Items.Add(item);
+ this.ContextMenu = contextMenu;
+ }
+ else
+ {
+
+ this.ContextMenu = null;
+ }
+ }
+
+ private void Item_Click(object sender, RoutedEventArgs e)
+ {
+ string manualDebugSeq = ParameterManager.GetCommonParameterByName("ManualDebugSeqPath")?.Value;
+ if (string.IsNullOrEmpty(manualDebugSeq))
+ {
+ MsgBoxHelper2.Error("璇峰厛璁剧疆鎵嬪姩璋冭瘯搴忓垪璺緞[ManualDebugSeqPath]!");
+ return;
+ }
+ string modelPath = parent.tsModelPath;
+ var win = new ManualDebugWindow(manualDebugSeq, modelPath, false)
+ {
+ Title = $"宸ヤ綅{this.index}鎵嬪姩璋冭瘯",
+
+ Owner = Window.GetWindow(this),
+ OvenIndex = 0,
+ OvenName = "",
+ CellCount = parent.tsSockets.Count,
+
+ CellIndex = this.index,
+ CellName = "",
+ };
+ win.ShowDialog();
+ }
+
+ private void ContextMenu_Closed(object sender, RoutedEventArgs e)
+ {
+ this.ContextMenu = null;
+ }
}
}
diff --git a/Plugin.TsExecution/TsExecution.xaml b/Plugin.TsExecution/TsExecution.xaml
index 269bb3c..ee28281 100644
--- a/Plugin.TsExecution/TsExecution.xaml
+++ b/Plugin.TsExecution/TsExecution.xaml
@@ -89,6 +89,10 @@
IsChecked="{Binding UI_ShowInfoDock,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
Content="{DynamicResource Ts.UI_ShowInfoDock}"/>
+ <CheckBox FontSize="20" x:Name="Cb_ManualDebug" Visibility="Collapsed"
+ IsChecked="{Binding EnableManualDebug,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
+ VerticalContentAlignment="Center" Content="鎵嬪姩璋冭瘯" />
+
<StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5">
<Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0"
Content="{DynamicResource HMI.User.Code}" />
diff --git a/Plugin.TsExecution/TsExecution.xaml.cs b/Plugin.TsExecution/TsExecution.xaml.cs
index 9575fb8..06b9d39 100644
--- a/Plugin.TsExecution/TsExecution.xaml.cs
+++ b/Plugin.TsExecution/TsExecution.xaml.cs
@@ -13,6 +13,7 @@
using NationalInstruments.TestStand.Interop.API;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
+using OpenTap;
using Plugin.TsSupport;
using ScottPlot;
using ScottPlot.Plottables;
@@ -24,7 +25,6 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
-using System.Windows.Forms.DataVisualization.Charting;
namespace Plugin.TsExecution
{
@@ -57,7 +57,7 @@
#endregion
public static ILog logger { get; private set; }
- private List<SingleControl> tsSockets = new List<SingleControl>();
+ public List<SingleControl> tsSockets { get; private set; } = new List<SingleControl>();
/// <summary>
/// 鏍蜂欢妯″紡鐘舵��
/// </summary>
@@ -298,7 +298,22 @@
}
}
- private string tsModelPath;
+ private bool enableManualDebug;
+ public bool EnableManualDebug
+ {
+ get => enableManualDebug;
+ set
+ {
+ if (IsTesting)
+ {
+ MsgBoxHelper2.Warn("璇峰厛缁撴潫鑷姩娴嬭瘯");
+ return;
+ }
+ Set(ref enableManualDebug, value);
+ }
+ }
+
+ public string tsModelPath { get; private set; }
private string dataLogPath;
private SqliteHelper sqliteHelper;
@@ -309,9 +324,7 @@
{
DataContext = this;
InitializeComponent();
-
InitChart();
- InitTsEngine();
}
public override void SetBaseFolder(string folder)
@@ -363,6 +376,7 @@
public override void InitFinish()
{
+ InitTsEngine();
this.ReadCount(out int pass, out int fail);
this.DisplayCount(pass, fail);
ReadConfig();
@@ -388,6 +402,33 @@
catch (Exception ex)
{
log.Warn("Get Login User Failed.", ex);
+ }
+ RefreshUIConfig();
+ }
+
+ /// <summary>
+ /// 鍒锋柊UI閰嶇疆
+ /// </summary>
+ private void RefreshUIConfig()
+ {
+ try
+ {
+ bool enableManualDebug = ParameterManager.GetCommonParameterByName("EnableManualDebug", false);
+ if (enableManualDebug)
+ {
+ Cb_ManualDebug.Visibility = Visibility.Visible;
+ EnableManualDebug = false;
+ }
+ else
+ {
+ Cb_ManualDebug.Visibility = Visibility.Collapsed;
+ EnableManualDebug = false;
+ }
+
+ }
+ catch (Exception ex)
+ {
+ log.Error("RefreshUIConfig Error.", ex);
}
}
@@ -498,7 +539,10 @@
{
if (ParamSetting.EngineType == "Tap")
{
- testExecutionWrapper = new TapWrapper();
+ var wrapper = new TapWrapper();
+ wrapper.RegisteUserInput(this);
+ log.Debug($"Wrapper UI is {wrapper.ui}");
+ testExecutionWrapper = wrapper;
}
else
{
@@ -657,6 +701,7 @@
{
SocketName = name
};
+ control.parent = this;
panel.Children.Add(control);
tsSockets.Add(control);
@@ -874,7 +919,7 @@
{
value = ParameterManager.GetCommonParameterByName("TestReportFolder");
- csvDir = value.Value;
+ csvDir = value?.Value ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestReport");
if (!Directory.Exists(csvDir))
{
Directory.CreateDirectory(csvDir);
@@ -889,7 +934,7 @@
{
value = ParameterManager.GetCommonParameterByName("IsOptRequirePassword");
- IsOptRequirePassword = bool.Parse(value.Value);
+ IsOptRequirePassword = value == null ? false : bool.Parse(value.Value);
}
catch
{
@@ -1218,9 +1263,12 @@
public void UpdatePositionStatus(int index, string status)
{
- this.tsSockets[index].UpdateStatus(status);
- OnStatusPublished(index, status);
- this.UpdateTestCounter();
+ Dispatcher.Invoke(() =>
+ {
+ this.tsSockets[index].UpdateStatus(status);
+ OnStatusPublished(index, status);
+ this.UpdateTestCounter();
+ });
}
private void OnStatusPublished(int index, string status)
@@ -1531,17 +1579,26 @@
break;
case 10006://鍙戦�佷骇鍝佹潯鐮�
- SetPositionBarcode(index, strData);
+ Dispatcher.Invoke(() =>
+ {
+ SetPositionBarcode(index, strData);
+ });
break;
case 10007://寮�濮嬫祴璇曡鏃�
- tsSockets[index].ElapseTime = "Start Timing";
- BtnStopSeq.IsEnabled = false;
+ Dispatcher.Invoke(() =>
+ {
+ tsSockets[index].ElapseTime = "Start Timing";
+ BtnStopSeq.IsEnabled = false;
+ });
break;
case 10008://鍋滄娴嬭瘯璁℃椂
- tsSockets[index].ElapseTime = "Stop Timing";
- BtnStopSeq.IsEnabled = true;
+ Dispatcher.Invoke(() =>
+ {
+ tsSockets[index].ElapseTime = "Stop Timing";
+ BtnStopSeq.IsEnabled = true;
+ });
break;
case 10009:
@@ -1582,7 +1639,10 @@
break;
case 10017://鏇存柊娴嬭瘯璁℃椂锛岄拡瀵归儴鍒嗚鏃舵湁PLC瀹屾垚鐨勯」鐩�
- this.tsSockets[index].ElapseTime = strData;
+ Dispatcher.Invoke(() =>
+ {
+ this.tsSockets[index].ElapseTime = strData;
+ });
break;
case (int)UIMessageCodes.UIMsg_EndExecution://鎵ц缁撴潫
//IsTesting = false;
@@ -1590,7 +1650,10 @@
break;
case 10030://鏄剧ず娴嬭瘯杩涘害
int ProcessVal = Convert.ToInt32(strData);
- this.tsSockets[index].SetTestProcess(ProcessVal);
+ Dispatcher.Invoke(() =>
+ {
+ this.tsSockets[index].SetTestProcess(ProcessVal);
+ });
break;
case 10031://瀵煎嚭CSV
@@ -1806,6 +1869,7 @@
public override void DoClose(object sender, EventArgs e)
{
+ testExecutionWrapper?.Dispose();
base.DoClose(sender, e);
}
diff --git a/Plugin.TsSupport/ITestExecutionWrapper.cs b/Plugin.TsSupport/ITestExecutionWrapper.cs
index ccd000b..66731f7 100644
--- a/Plugin.TsSupport/ITestExecutionWrapper.cs
+++ b/Plugin.TsSupport/ITestExecutionWrapper.cs
@@ -42,5 +42,7 @@
bool ReleaseSeqFile();
void SetContext(object context);
+
+ void Dispose();
}
}
diff --git a/Plugin.TsSupport/Plugin.TsSupport.csproj b/Plugin.TsSupport/Plugin.TsSupport.csproj
index 4b296a0..f18dbe2 100644
--- a/Plugin.TsSupport/Plugin.TsSupport.csproj
+++ b/Plugin.TsSupport/Plugin.TsSupport.csproj
@@ -81,6 +81,7 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath>
</Reference>
+ <Reference Include="PresentationFramework" />
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Windows.Forms" />
@@ -90,6 +91,7 @@
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
+ <Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="ITestExecutionWrapper.cs" />
@@ -97,7 +99,20 @@
<Compile Include="TestAppManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TeststandWrapper.cs" />
- <Compile Include="TsExecutionWrapper.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj">
+ <Project>{4BCFADC0-54F9-7032-B918-E4DD407CAA76}</Project>
+ <Name>Tap.Plugins.BasicSteps</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj">
+ <Project>{EFBD516D-3DC9-9D75-F082-48FBC76715F7}</Project>
+ <Name>Tap.Engine</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\..\..\Project\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj">
+ <Project>{0d150ca1-d6ea-3362-7330-028939e66e1c}</Project>
+ <Name>AddInPlugin</Name>
+ </ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
\ No newline at end of file
diff --git a/Plugin.TsSupport/TapWrapper.cs b/Plugin.TsSupport/TapWrapper.cs
index 23ef29a..c676291 100644
--- a/Plugin.TsSupport/TapWrapper.cs
+++ b/Plugin.TsSupport/TapWrapper.cs
@@ -1,39 +1,188 @@
-锘縰sing System;
-//using AddInPlugin;
-//using AddInPlugin.Util;
-//using Microsoft.Win32;
-//using OpenTap;
-//using OpenTap.Addin;
-//using OpenTap.Addin.Util;
-//using System.ComponentModel;
-//using System.IO;
+锘縰sing AddInPlugin;
+using AddInPlugin.Util;
+using Microsoft.Win32;
+using OpenTap;
+using OpenTap.Addin;
+using OpenTap.Addin.Listener;
+using OpenTap.Addin.Reader;
+using OpenTap.Addin.Util;
+using OpenTap.Plugins.BasicSteps;
+using System;
+using System.ComponentModel;
+using System.IO;
+using System.Windows;
+using System.Windows.Threading;
namespace Plugin.TsSupport
{
-
- public class TapWrapper : ITestExecutionWrapper
+ public class Net47Getter : IRegGetter
{
+ public string GetAppPath(string keyPath)
+ {
+ using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath))
+ {
+ if (key == null)
+ {
+ throw new Exception("娉ㄥ唽琛ㄤ笉瀛樺湪锛岃鍏堣繍琛屼竴娆$紪杈戝櫒");
+ }
+
+ string value = key.GetValue("Location")?.ToString();
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new Exception("娉ㄥ唽琛ㄤ笉瀛樺湪锛岃鍏堣繍琛屼竴娆$紪杈戝櫒");
+ }
+ return value;
+ }
+ }
+ }
+
+ public class TapWrapper : ITestExecutionWrapper, IResultListener, IUserInputInterface
+ {
+ TapThread testPlanThread;
+ TestPlan Plan;
+ TestPlanRun PlanRun;
+
+ public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<UIMessageEvent> UIMessageEvent;
public event EventHandler<ExecutionEndEvent> EndExecutionEvent;
- public void CommitToDisk(bool option)
- {
+ private PostUIMessageHandler uIMessageHandler;
- }
+ public string Name { get; set; }
- public T GetFileGlobalValue<T>(string key, int option)
- {
- return default(T);
- }
+ public bool IsConnected => true;
- public T GetStationGlobalValue<T>(string key, int option)
+ static TapWrapper()
{
- return default(T);
+ RegUtil.Register(new Net47Getter());
}
public object Init()
{
+ StationGlobalsManager.Register(new DefaultStationGlobalsManager());
+ VariableResolverFactory.Register(new DefaultVariableResolverFactory());
+ ResultSettings.Current.Add(this);
+ UserInput.SetInterface(this);
+ LvlibpLoader.PreLoad();
+ PropertyInfoCache.Refresh(typeof(MethodVariable));
+ uIMessageHandler = PostUIMessageHandler.Get();
+ uIMessageHandler.OnPostUIMsg += PostUIMsgHandler_OnPostUIMsg;
return null;
+ }
+
+ private void PostUIMsgHandler_OnPostUIMsg(object sender, PostUIMsg e)
+ {
+ UIMessageEvent?.Invoke(this, new UIMessageEvent()
+ {
+ Origin = e,
+ EventCode = e.EventCode,
+ BoolData = e.BoolData,
+ NumberData = e.NumberData,
+ StrData = e.StringData
+ });
+ }
+
+ public object StartSequence(string filePath, string modelPath)
+ {
+ Plan = TestPlanReader.ReadTestPlan(filePath);
+ //Plan = TestPlan.Load(filePath);
+ testPlanThread = TapThread.Start(() =>
+ {
+ var res = ResultSettings.Current;
+ res.Add(uIMessageHandler);
+ Plan.Execute(ResultSettings.Current);
+ });
+ return Plan;
+ }
+
+
+ public T GetFileGlobalValue<T>(string key, int option)
+ {
+ return PlanRun.Resolve<T>(null, $"FileGlobals.{key}");
+ }
+
+ public T GetStationGlobalValue<T>(string key, int option)
+ {
+ if (PlanRun == null)
+ {
+ return StationGlobalsManager.GetValue<T>(key);
+ }
+ return PlanRun.Resolve<T>(null, $"StationGlobals.{key}");
+ }
+
+ public T GetValue<T>(string key, int option)
+ {
+ return default(T);
+ }
+
+ public void SetValue<T>(string key, T value, int option)
+ {
+
+ }
+
+ public void SetFileGlobalValue<T>(string key, int option, T value)
+ {
+ PlanRun.Update(null, $"FileGlobals.{key}", value);
+ }
+
+ public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
+ {
+ if (PlanRun == null)
+ {
+ StationGlobalsManager.SetValue(key, value);
+ }
+ else
+ {
+ PlanRun.Update(null, $"StationGlobals.{key}", value);
+ }
+ }
+
+ public void CommitToDisk(bool option)
+ {
+ StationGlobalsManager.WriteToLocal(null);
+ }
+
+ public void OnTestPlanRunStart(TestPlanRun planRun)
+ {
+ PlanRun = planRun;
+ }
+
+ public void OnTestPlanRunCompleted(TestPlanRun planRun, Stream logStream)
+ {
+ PlanRun = null;
+ }
+
+ public void OnTestStepRunStart(TestStepRun stepRun)
+ {
+
+ }
+
+ public void OnTestStepRunCompleted(TestStepRun stepRun)
+ {
+
+ }
+
+ public void OnResultPublished(Guid stepRunID, ResultTable result)
+ {
+
+ }
+
+ public void Open()
+ {
+
+ }
+
+ public void Close()
+ {
+ EndExecutionEvent?.Invoke(this, new ExecutionEndEvent());
+ Plan = null;
+ PlanRun = null;
+ testPlanThread = null;
+ }
+
+ public bool WaitForExit()
+ {
+ return true;
}
public bool ReleaseSeqFile()
@@ -41,198 +190,48 @@
return true;
}
+ public void Dispose()
+ {
+ }
+
public void SetContext(object context)
{
}
- public void SetFileGlobalValue<T>(string key, int option, T value)
- {
+ public DispatcherObject ui { get; private set; }
+ public void RegisteUserInput(DispatcherObject ui)
+ {
+ this.ui = ui;
}
- public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
+ public void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal)
{
-
- }
-
- public object StartSequence(string filePath, string modelPath)
- {
- return null;
- }
-
- public bool WaitForExit()
- {
- return true;
+ if (dataObject is DialogRequest req)
+ {
+ this.ui?.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ if (req.Buttons == InputButtons.OkCancel)
+ {
+ var res = MessageBox.Show(req.Message, req.Name, MessageBoxButton.OKCancel);
+ req.Input2 = res == MessageBoxResult.OK ? WaitForInputResult2.Ok : WaitForInputResult2.Cancel;
+ }
+ else
+ {
+ var res = MessageBox.Show(req.Message, req.Name, MessageBoxButton.YesNo);
+ req.Input1 = res == MessageBoxResult.Yes ? WaitForInputResult1.Yes : WaitForInputResult1.No;
+ }
+ })).Wait();
+ }
+ else if (dataObject is OpenTap.TestPlan.PlanLoadError err)
+ {
+ this.ui?.Dispatcher.BeginInvoke(new Action(() =>
+ {
+ var res = MessageBox.Show(err.Message, err.Name, MessageBoxButton.OKCancel);
+ err.Response = res == MessageBoxResult.OK ? TestPlan.PlanLoadErrorResponse.Ignore : TestPlan.PlanLoadErrorResponse.Abort;
+ })).Wait();
+ }
}
}
-
- //public class Net47Getter : IRegGetter
- //{
- // public string GetAppPath(string keyPath)
- // {
- // using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath))
- // {
- // if (key == null)
- // {
- // throw new Exception("娉ㄥ唽琛ㄤ笉瀛樺湪锛岃鍏堣繍琛屼竴娆$紪杈戝櫒");
- // }
-
- // string value = key.GetValue("Location")?.ToString();
- // if (string.IsNullOrEmpty(value))
- // {
- // throw new Exception("娉ㄥ唽琛ㄤ笉瀛樺湪锛岃鍏堣繍琛屼竴娆$紪杈戝櫒");
- // }
- // return value;
- // }
- // }
- //}
-
- //public class TapWrapper : ITestExecutionWrapper, IResultListener
- //{
- // TapThread testPlanThread;
- // TestPlan Plan;
- // TestPlanRun PlanRun;
-
- // public event PropertyChangedEventHandler PropertyChanged;
-
- // public event EventHandler<UIMessageEvent> UIMessageEvent;
- // public event EventHandler<ExecutionEndEvent> EndExecutionEvent;
-
- // public string Name { get; set; }
-
- // public bool IsConnected => true;
-
- // public object Init()
- // {
- // RegUtil.Register(new Net47Getter());
- // StationGlobalsManager.Register(new DefaultStationGlobalsManager());
- // VariableResolverFactory.Register(new DefaultVariableResolverFactory());
- // ResultSettings.Current.Add(this);
- // return null;
- // }
-
- // private void PostUIMsgHandler_OnPostUIMsg(object sender, PostUIMsg e)
- // {
- // UIMessageEvent?.Invoke(this, new UIMessageEvent()
- // {
- // Origin = e,
- // EventCode = e.EventCode,
- // BoolData = e.BoolData,
- // NumberData = e.NumberData,
- // StrData = e.StringData
- // });
- // }
-
- // public object StartSequence(string filePath, string modelPath)
- // {
- // Plan = TestPlan.Load(filePath);
- // testPlanThread = TapThread.Start(() =>
- // {
- // var res = ResultSettings.Current;
- // Plan.Execute(ResultSettings.Current, null);
- // });
- // return Plan;
- // }
-
-
- // public T GetFileGlobalValue<T>(string key, int option)
- // {
- // return PlanRun.Resolve<T>(null, $"FileGlobals.{key}");
- // }
-
- // public T GetStationGlobalValue<T>(string key, int option)
- // {
- // if (PlanRun == null)
- // {
- // return StationGlobalsManager.GetValue<T>(key);
- // }
- // return PlanRun.Resolve<T>(null, $"StationGlobals.{key}");
- // }
-
- // public T GetValue<T>(string key, int option)
- // {
- // return default(T);
- // }
-
- // public void SetValue<T>(string key, T value, int option)
- // {
-
- // }
-
- // public void SetFileGlobalValue<T>(string key, int option, T value)
- // {
- // PlanRun.Update(null, $"FileGlobals.{key}", value);
- // }
-
- // public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
- // {
- // if (PlanRun == null)
- // {
- // StationGlobalsManager.SetValue(key, value);
- // }
- // else
- // {
- // PlanRun.Update(null, $"StationGlobals.{key}", value);
- // }
- // }
-
- // public void CommitToDisk(bool option)
- // {
- // StationGlobalsManager.WriteToLocal(null);
- // }
-
- // public void OnTestPlanRunStart(TestPlanRun planRun)
- // {
- // PlanRun = planRun;
- // }
-
- // public void OnTestPlanRunCompleted(TestPlanRun planRun, Stream logStream)
- // {
-
-
- // }
-
- // public void OnTestStepRunStart(TestStepRun stepRun)
- // {
-
- // }
-
- // public void OnTestStepRunCompleted(TestStepRun stepRun)
- // {
-
- // }
-
- // public void OnResultPublished(Guid stepRunID, ResultTable result)
- // {
-
- // }
-
- // public void Open()
- // {
- // PostUIMsgHandler.OnPostUIMsg += PostUIMsgHandler_OnPostUIMsg;
- // }
-
- // public void Close()
- // {
- // PostUIMsgHandler.OnPostUIMsg -= PostUIMsgHandler_OnPostUIMsg;
- // EndExecutionEvent?.Invoke(this, new ExecutionEndEvent());
- // PlanRun = null;
- // }
-
- // public bool WaitForExit()
- // {
- // return true;
- // }
-
- // public bool ReleaseSeqFile()
- // {
- // return true;
- // }
-
- // public void SetContext(object context)
- // {
-
- // }
- //}
}
diff --git a/Plugin.TsSupport/TestAppManager.cs b/Plugin.TsSupport/TestAppManager.cs
index f136b8f..ecc405c 100644
--- a/Plugin.TsSupport/TestAppManager.cs
+++ b/Plugin.TsSupport/TestAppManager.cs
@@ -17,5 +17,17 @@
return instance;
}
}
+
+ public static void Dispose()
+ {
+ lock (lockObj)
+ {
+ if (instance != null)
+ {
+ instance.Dispose();
+ instance = null;
+ }
+ }
+ }
}
}
diff --git a/Plugin.TsSupport/TeststandWrapper.cs b/Plugin.TsSupport/TeststandWrapper.cs
index ea15087..1b7bfec 100644
--- a/Plugin.TsSupport/TeststandWrapper.cs
+++ b/Plugin.TsSupport/TeststandWrapper.cs
@@ -82,12 +82,17 @@
return this.AxApplicationMgr;
}
+ private SequenceFile GetFile(string path)
+ {
+ return null;
+ }
+
public object StartSequence(string filePath, string modelPath)
{
this.testSeqFile = filePath;
this.modeSeq = this.engine.GetSequenceFileEx(modelPath);
- this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
+ this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 107, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
return execution;
}
@@ -232,5 +237,12 @@
{
this.context = (SequenceContext)context;
}
+
+ public void Dispose()
+ {
+ AxApplicationMgr = null;
+ engine = null;
+ TestAppManager.Dispose();
+ }
}
}
diff --git a/Plugin.TsSupport/TsExecutionWrapper.cs b/Plugin.TsSupport/TsExecutionWrapper.cs
deleted file mode 100644
index 1298dd5..0000000
--- a/Plugin.TsSupport/TsExecutionWrapper.cs
+++ /dev/null
@@ -1,228 +0,0 @@
-锘縰sing NationalInstruments.TestStand.Interop.API;
-using NationalInstruments.TestStand.Interop.UI.Ax;
-using System;
-
-namespace Plugin.TsSupport
-{
- public class TsExecutionWrapper : ITestExecutionWrapper
- {
-
- #region TestStand浜嬩欢
- /// <summary>
- /// TS 寮曟搸
- /// </summary>
- private Engine engine = null;
-
- /// <summary>
- /// 娴嬭瘯搴忓垪璺緞
- /// </summary>
- private string testSeqFile = string.Empty;
-
- /// <summary>
- /// 娴嬭瘯搴忓垪
- /// </summary>
- private SequenceFile testSeq = null;
-
- /// <summary>
- /// 娴嬭瘯妯″瀷搴忓垪
- /// </summary>
- private SequenceFile modeSeq = null;
-
- /// <summary>
- /// 杩愯瀹炰緥
- /// </summary>
- private Execution execution = null;
-
- public SequenceContext context { get; set; } = null;
-
- /// <summary>
- /// 浜嬩欢绠$悊鍣�
- /// </summary>
- public AxApplicationMgr AxApplicationMgr { get; private set; }
-
- public event EventHandler<UIMessageEvent> UIMessageEvent;
- public event EventHandler<ExecutionEndEvent> EndExecutionEvent;
-
- public TsExecutionWrapper()
- {
- AxApplicationMgr = TestAppManager.GetAxApplicationMgr();
- AxApplicationMgr.EndExecution += AxApplicationMgr_EndExecution;
- //AxApplicationMgr.ExitApplication += new EventHandler(this.AxApplicationMgr_ExitApplication);
- AxApplicationMgr.UIMessageEvent += AxApplicationMgr_UIMessageEvent;
- //AxApplicationMgr.ExecutionClosed +=
- // new _ApplicationMgrEvents_ExecutionClosedEventHandler(this.AxApplicationMgr_ExecutionClosed);
- }
-
- #endregion
-
- private void AxApplicationMgr_EndExecution(object sender, _ApplicationMgrEvents_EndExecutionEvent e)
- {
- EndExecutionEvent?.Invoke(this, new ExecutionEndEvent() { Origin = e });
- }
-
- private void AxApplicationMgr_UIMessageEvent(object sender, _ApplicationMgrEvents_UIMessageEventEvent e)
- {
- UIMessageEvent?.Invoke(this, new UIMessageEvent()
- {
- Origin = e,
- EventCode = Convert.ToInt32(e.uiMsg.Event),
- NumberData = e.uiMsg.NumericData,
- Data = e.uiMsg.ActiveXData,
- StrData = e.uiMsg.StringData
- });
- }
-
- public object Init()
- {
- this.engine = this.AxApplicationMgr.GetEngine();
- this.engine.PersistBreakpoints = false;
- this.AxApplicationMgr.IsEditor = true;
- this.AxApplicationMgr.LoginOnStart = false;
- this.AxApplicationMgr.Start();
- return this.AxApplicationMgr;
- }
-
- public object StartSequence(string filePath, string modelPath)
- {
- this.testSeqFile = filePath;
-
- this.modeSeq = this.engine.GetSequenceFileEx(modelPath);
- this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType);
- this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null);
- return execution;
- }
-
- public T GetFileGlobalValue<T>(string key, int option)
- {
- if (typeof(T) == typeof(bool))
- {
- return (T)(object)context?.FileGlobals?.GetValBoolean(key, option);
- }
- else if (typeof(T) == typeof(string))
- {
- return (T)(object)context?.FileGlobals?.GetValString(key, option);
- }
- else if (typeof(T) == typeof(int))
- {
- return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
- }
- else if (typeof(T) == typeof(double))
- {
- return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
- }
- else if (typeof(T) == typeof(float))
- {
- return (T)(object)context?.FileGlobals?.GetValNumber(key, option);
- }
- return default(T);
- }
-
- public T GetStationGlobalValue<T>(string key, int option)
- {
- if (typeof(T) == typeof(bool))
- {
- return (T)(object)engine?.Globals?.GetValBoolean(key, option);
- }
- else if (typeof(T) == typeof(string))
- {
- return (T)(object)engine?.Globals?.GetValString(key, option);
- }
- else if (typeof(T) == typeof(int))
- {
- return (T)(object)engine?.Globals?.GetValNumber(key, option);
- }
- else if (typeof(T) == typeof(double))
- {
- return (T)(object)engine?.Globals?.GetValNumber(key, option);
- }
- else if (typeof(T) == typeof(float))
- {
- return (T)(object)engine?.Globals?.GetValNumber(key, option);
- }
- return default(T);
- }
-
- public void SetFileGlobalValue<T>(string key, int option, T value)
- {
- if (value is bool bVal)
- {
- context?.FileGlobals?.SetValBoolean(key, option, bVal);
- }
- else if (value is string sVal)
- {
- context?.FileGlobals?.SetValString(key, option, sVal);
- }
- else if (value is int iVal)
- {
- context?.FileGlobals?.SetValNumber(key, option, iVal);
- }
- else if (value is double dVal)
- {
- context?.FileGlobals?.SetValNumber(key, option, dVal);
- }
- else if (value is float fVal)
- {
- context?.FileGlobals?.SetValNumber(key, option, fVal);
- }
- }
-
- public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false)
- {
- PropertyObject obj = null;
- if (inContext)
- {
- obj = context?.StationGlobals;
- }
- else
- {
- obj = engine?.Globals;
- }
-
- if (value is bool bVal)
- {
- obj?.SetValBoolean(key, option, bVal);
- }
- else if (value is string sVal)
- {
- obj?.SetValString(key, option, sVal);
- }
- else if (value is int iVal)
- {
- obj?.SetValNumber(key, option, iVal);
- }
- else if (value is double dVal)
- {
- obj?.SetValNumber(key, option, dVal);
- }
- else if (value is float fVal)
- {
- obj?.SetValNumber(key, option, fVal);
- }
- //engine?.CommitGlobalsToDisk(promptOnSaveConflicts: false);
- }
-
- public void CommitToDisk(bool option)
- {
- engine?.CommitGlobalsToDisk(option);
- }
-
- public bool WaitForExit()
- {
- return this.execution?.WaitForEndEx(-1, true, Type.Missing, null) == true;
- }
-
- public bool ReleaseSeqFile()
- {
- if (testSeq != null)
- {
- return engine?.ReleaseSequenceFileEx(testSeq, 4) == true;
- }
- return true;
- }
-
- public void SetContext(object context)
- {
- this.context = (SequenceContext)context;
- }
- }
-}
diff --git a/Plugin.UICollection/Plugin.UICollection.csproj b/Plugin.UICollection/Plugin.UICollection.csproj
index 97514d3..d960a7e 100644
--- a/Plugin.UICollection/Plugin.UICollection.csproj
+++ b/Plugin.UICollection/Plugin.UICollection.csproj
@@ -17,8 +17,8 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\UILib\UILib.csproj" />
- <ProjectReference Include="..\..\UtilLib\UtilLib.csproj" />
+ <ProjectReference Include="..\..\..\VisualStudio\UILib\UILib.csproj" />
+ <ProjectReference Include="..\..\..\VisualStudio\UtilLib\UtilLib.csproj" />
<ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
</ItemGroup>
diff --git a/SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj b/SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj
index 3407e37..9fb19e0 100644
--- a/SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj
+++ b/SimpleUtil/HMI.LogUtil/HMI.LogUtil.csproj
@@ -52,7 +52,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data.SQLite">
- <HintPath>..\..\..\HMI.Old\Plugin_OPCServer\Libs\System.Data.SQLite.dll</HintPath>
+ <HintPath>..\..\Libs\System.Data.SQLite.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
diff --git a/Util.ManualDebug/ManualCell.xaml b/Util.ManualDebug/ManualCell.xaml
new file mode 100644
index 0000000..e5d12cf
--- /dev/null
+++ b/Util.ManualDebug/ManualCell.xaml
@@ -0,0 +1,33 @@
+锘�<UserControl x:Class="Util.ManualDebug.ManualCell"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Util.ManualDebug"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ mc:Ignorable="d" Padding="5" MaxHeight="80"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UserControl.Style>
+ <Style TargetType="UserControl">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Selected}" Value="True">
+ <Setter Property="Background" Value="{DynamicResource SuccessColor}"/>
+ <Setter Property="Foreground" Value="#FFF"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </UserControl.Style>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <TextBlock FontSize="20" FontWeight="Bold" Text="{Binding Title}"/>
+ <pu:Switch Grid.Row="1" FontSize="18" BoxHeight="20" Cursor="Hand"
+ Content="鍏�"
+ CheckedBackground="Green"
+ Background="Red"
+ CheckedContent="寮�"
+ IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
+ </Grid>
+</UserControl>
diff --git a/Util.ManualDebug/ManualCell.xaml.cs b/Util.ManualDebug/ManualCell.xaml.cs
new file mode 100644
index 0000000..3de1495
--- /dev/null
+++ b/Util.ManualDebug/ManualCell.xaml.cs
@@ -0,0 +1,66 @@
+锘縰sing System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Controls;
+
+namespace Util.ManualDebug
+{
+ /// <summary>
+ /// ManualCell.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class ManualCell : UserControl, INotifyPropertyChanged
+ {
+ #region ...
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void Set([CallerMemberName] string name = null)
+ {
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
+ {
+ old = @new;
+ if (propertyName != null)
+ {
+ Set(propertyName);
+ }
+ }
+ #endregion
+ private string title;
+ public string Title
+ {
+ get => title;
+ set => Set(ref title, value);
+ }
+
+ private bool selected;
+ public bool Selected
+ {
+ get => selected;
+ set
+ {
+ if (selected != value)
+ {
+ Set(ref selected, value);
+ SelectedChanged?.Invoke(this, new EventArgs());
+ }
+ }
+ }
+
+ private int index;
+ public int Index
+ {
+ get => index; set => Set(ref index, value);
+ }
+
+ public event EventHandler SelectedChanged;
+
+ public ManualCell()
+ {
+ this.DataContext = this;
+ InitializeComponent();
+ }
+
+ }
+}
diff --git a/Util.ManualDebug/ManualDebug.xaml b/Util.ManualDebug/ManualDebug.xaml
index ca818b0..32df8d8 100644
--- a/Util.ManualDebug/ManualDebug.xaml
+++ b/Util.ManualDebug/ManualDebug.xaml
@@ -1,9 +1,10 @@
-<UserControl x:Class="Util.SB.ManualDebug"
+<UserControl x:Class="Util.ManualDebug.ManualDebug"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:local="clr-namespace:Util.SB"
+ xmlns:local="clr-namespace:Util.ManualDebug"
+ xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"
mc:Ignorable="d"
x:Name="self"
MinHeight="400" MinWidth="500">
@@ -17,104 +18,149 @@
</UserControl.Resources>
<Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition Height="Auto"/>
+ </Grid.RowDefinitions>
<Grid Margin="10">
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="5"/>
- <RowDefinition Height="*"/>
- <RowDefinition Height="5"/>
- <RowDefinition Height="*"/>
- </Grid.RowDefinitions>
- <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost>
+ <Grid.ColumnDefinitions>
+ <ColumnDefinition Width="Auto"/>
+ <ColumnDefinition/>
+ </Grid.ColumnDefinitions>
- <!-- 鎸夐挳鍜岀姸鎬佸尯鍩� -->
- <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}"
+ <Grid x:Name="CellSelectBorder">
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+
+ <Border Background="{StaticResource TsPanelBrush}" Margin="0,0,10,5"
+BorderBrush="{StaticResource BorderBrush}"
+BorderThickness="1" CornerRadius="4" Padding="15">
+ <StackPanel>
+ <TextBlock Text="绌翠綅閫夋嫨" FontSize="14" FontWeight="SemiBold"
+ Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/>
+
+ <UniformGrid IsEnabled="False" x:Name="CellGrid" dp:UniformGridHelper.InnerBorder="1" Columns="1"
+ dp:UniformGridHelper.InnerBorderBrush="{StaticResource BorderBrush}">
+
+ </UniformGrid>
+ </StackPanel>
+ </Border>
+
+ <Border Grid.Row="1" Background="{StaticResource TsPanelBrush}" Margin="0,5,10,0"
+BorderBrush="{StaticResource BorderBrush}"
+BorderThickness="1" CornerRadius="4" Padding="15">
+ <StackPanel>
+ <TextBlock Text="绌翠綅娴嬭瘯缁撴灉" FontSize="14" FontWeight="SemiBold"
+ Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/>
+
+ <UniformGrid IsEnabled="False" x:Name="CellResultGrid" Columns="1" dp:UniformGridHelper.InnerBorder="1"
+ dp:UniformGridHelper.InnerBorderBrush="{StaticResource BorderBrush}">
+
+ </UniformGrid>
+ </StackPanel>
+ </Border>
+ </Grid>
+
+ <Grid Grid.Column="1">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="5"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="5"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost>
+
+ <!-- 鎸夐挳鍜岀姸鎬佸尯鍩� -->
+ <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" CornerRadius="4" Padding="15">
- <StackPanel>
- <TextBlock Text="鎵嬪姩璋冭瘯鎺у埗" FontSize="14" FontWeight="SemiBold"
+ <StackPanel>
+ <TextBlock Text="鎵嬪姩璋冭瘯鎺у埗" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/>
- <WrapPanel x:Name="btnPanel">
+ <WrapPanel x:Name="btnPanel">
- </WrapPanel>
- </StackPanel>
- </Border>
+ </WrapPanel>
+ </StackPanel>
+ </Border>
- <!-- 鍒嗛殧鏉�1 -->
- <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
+ <!-- 鍒嗛殧鏉�1 -->
+ <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
Background="Transparent" Cursor="SizeNS"/>
- <!-- TX/RX 鏁版嵁鍖哄煙 -->
- <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}"
+ <!-- TX/RX 鏁版嵁鍖哄煙 -->
+ <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" CornerRadius="4" Padding="10">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="*"/>
- </Grid.RowDefinitions>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
- <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10">
- <TextBlock Text="娴嬭瘯鏁版嵁" FontSize="14" FontWeight="SemiBold"
+ <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10">
+ <TextBlock Text="娴嬭瘯鏁版嵁" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
- <Button Content="娓呯┖" Style="{StaticResource ActionButtonStyle}"
+ <Button Content="娓呯┖" Style="{StaticResource ActionButtonStyle}"
Margin="15,0,0,0" Click="OnClearClick"/>
- </StackPanel>
+ </StackPanel>
- <DataGrid x:Name="DataGridComm" Grid.Row="1"
+ <DataGrid x:Name="DataGridComm" Grid.Row="1"
ItemsSource="{Binding TestData,ElementName=self}">
- <DataGrid.Columns>
- <DataGridTextColumn Header="娴嬭瘯椤�" Binding="{Binding Name}" Width="120" IsReadOnly="True"/>
- <DataGridTextColumn Header="鏀�" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/>
- <DataGridTextColumn Header="鍙�" Binding="{Binding Send}" Width="*" IsReadOnly="True"/>
- <DataGridTextColumn Header="鍗曚綅" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/>
- <DataGridTextColumn Header="涓婇檺" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/>
- <DataGridTextColumn Header="涓嬮檺" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/>
- <DataGridTextColumn Header="缁撴灉" Binding="{Binding Result}" Width="70" IsReadOnly="True"/>
- <DataGridTextColumn Header="鏃堕棿" Binding="{Binding Time}" Width="90" IsReadOnly="True"/>
- </DataGrid.Columns>
- <DataGrid.RowStyle>
- <Style TargetType="DataGridRow">
- <Style.Triggers>
- <DataTrigger Value="True">
- <DataTrigger.Binding>
- <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}">
- <Binding Path="Result"/>
- </MultiBinding>
- </DataTrigger.Binding>
- <Setter Property="Background" Value="#FFE0E0"/>
- <Setter Property="Foreground" Value="#C53030"/>
- </DataTrigger>
- </Style.Triggers>
- </Style>
- </DataGrid.RowStyle>
- </DataGrid>
- </Grid>
- </Border>
+ <DataGrid.Columns>
+ <DataGridTextColumn Header="娴嬭瘯椤�" Binding="{Binding Name}" Width="120" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鏀�" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鍙�" Binding="{Binding Send}" Width="*" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鍗曚綅" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/>
+ <DataGridTextColumn Header="涓婇檺" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/>
+ <DataGridTextColumn Header="涓嬮檺" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/>
+ <DataGridTextColumn Header="缁撴灉" Binding="{Binding Result}" Width="70" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鏃堕棿" Binding="{Binding Time}" Width="90" IsReadOnly="True"/>
+ </DataGrid.Columns>
+ <DataGrid.RowStyle>
+ <Style TargetType="DataGridRow">
+ <Style.Triggers>
+ <DataTrigger Value="True">
+ <DataTrigger.Binding>
+ <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}">
+ <Binding Path="Result"/>
+ </MultiBinding>
+ </DataTrigger.Binding>
+ <Setter Property="Background" Value="#FFE0E0"/>
+ <Setter Property="Foreground" Value="#C53030"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </DataGrid.RowStyle>
+ </DataGrid>
+ </Grid>
+ </Border>
- <!-- 鍒嗛殧鏉�2 -->
- <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch"
+ <!-- 鍒嗛殧鏉�2 -->
+ <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch"
Background="Transparent" Cursor="SizeNS"/>
- <!-- 鏃ュ織鍖哄煙 -->
- <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}"
+ <!-- 鏃ュ織鍖哄煙 -->
+ <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}"
BorderBrush="{StaticResource BorderBrush}"
BorderThickness="1" CornerRadius="4" Padding="10">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="*"/>
- </Grid.RowDefinitions>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
- <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
- <TextBlock Text="鏃ュ織" FontSize="14" FontWeight="SemiBold"
+ <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
+ <TextBlock Text="鏃ュ織" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
- <Button Content="娓呯┖鏃ュ織" Style="{StaticResource ActionButtonStyle}"
+ <Button Content="娓呯┖鏃ュ織" Style="{StaticResource ActionButtonStyle}"
Margin="15,0,0,0" Click="OnClearLogClick"/>
- </StackPanel>
+ </StackPanel>
- <RichTextBox x:Name="LogRichTextBox" Grid.Row="1"
+ <RichTextBox x:Name="LogRichTextBox" Grid.Row="1"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
FontFamily="Consolas"
@@ -122,12 +168,45 @@
BorderThickness="1"
BorderBrush="Black"
Padding="4"/>
- </Grid>
+ </Grid>
+ </Border>
+ </Grid>
+ <Border Background="#AAFFFFFF" Grid.ColumnSpan="2" x:Name="mask">
+ <TextBlock Text="鍒濆鍖栦腑..."
+ FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
- <Border Background="#AAFFFFFF" x:Name="mask">
- <TextBlock Text="鍒濆鍖栦腑..."
- FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
- </Border>
+
+ <StatusBar FontSize="14" Grid.Row="1">
+ <StatusBar.ItemsPanel>
+ <ItemsPanelTemplate>
+ <DockPanel LastChildFill="False"/>
+ </ItemsPanelTemplate>
+ </StatusBar.ItemsPanel>
+
+ <StatusBarItem DockPanel.Dock="Left">
+ <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="8">
+ <StackPanel Orientation="Horizontal">
+ <Ellipse Width="10" Height="10">
+ <Ellipse.Style>
+ <Style TargetType="Ellipse">
+ <Setter Property="Fill" Value="LightGray"/>
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding TcpServerStatus,ElementName=self}" Value="ERROR">
+ <Setter Property="Fill" Value="Red"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding TcpServerStatus,ElementName=self}" Value="RUNNING">
+ <Setter Property="Fill" Value="Green"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </Ellipse.Style>
+ </Ellipse>
+ <TextBlock Text="{Binding TcpServerInfo,ElementName=self}"/>
+ <Button Padding="0" Margin="0" BorderThickness="0" Click="Button_Click">馃攧</Button>
+ </StackPanel>
+ </StackPanel>
+ </StatusBarItem>
+ </StatusBar>
</Grid>
</UserControl>
diff --git a/Util.ManualDebug/ManualDebug.xaml.cs b/Util.ManualDebug/ManualDebug.xaml.cs
index 8c6b5ef..c625a09 100644
--- a/Util.ManualDebug/ManualDebug.xaml.cs
+++ b/Util.ManualDebug/ManualDebug.xaml.cs
@@ -1,14 +1,22 @@
+using HMI.Core.Util;
+using HMI.UI.DpUtil;
+using NationalInstruments.TestStand.Interop.API;
using Plugin.TsSupport;
+using SuperSocket.SocketBase.Config;
+using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Configuration;
using System.Globalization;
+using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
+using Util.ManualDebug.Model;
-namespace Util.SB
+namespace Util.ManualDebug
{
public class BtnConfig
{
@@ -45,9 +53,27 @@
Warn = 2,
Error = 3
}
-
- public partial class ManualDebug : UserControl
+
+ public partial class ManualDebug : UserControl, INotifyPropertyChanged
{
+ #region ...
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void Set([CallerMemberName] string name = null)
+ {
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
+ {
+ old = @new;
+ if (propertyName != null)
+ {
+ Set(propertyName);
+ }
+ }
+ #endregion
public static readonly DependencyProperty OvenNameProperty = DependencyProperty.Register(
"OvenName",
typeof(string),
@@ -68,6 +94,12 @@
public static readonly DependencyProperty CellIndexProperty = DependencyProperty.Register(
"CellIndex",
+ typeof(int),
+ typeof(ManualDebug),
+ new PropertyMetadata(-1));
+
+ public static readonly DependencyProperty CellCountProperty = DependencyProperty.Register(
+ "CellCount",
typeof(int),
typeof(ManualDebug),
new PropertyMetadata(0));
@@ -99,6 +131,12 @@
set { SetValue(CellIndexProperty, value); }
}
+ public int CellCount
+ {
+ get { return (int)GetValue(CellCountProperty); }
+ set { SetValue(CellCountProperty, value); }
+ }
+
public ObservableCollection<TestDataItem> TestData { get; private set; } = new ObservableCollection<TestDataItem>();
private Dictionary<string, StatusBtn> btnCache = new Dictionary<string, StatusBtn>();
@@ -111,8 +149,30 @@
private ITestExecutionWrapper executionWrapper;
+ private TcpServer tcpServer;
+
public string seqPath { get; set; }
public string modelPath { get; set; } = "C:\\Program Files\\National Instruments\\TestStand 2020\\Components\\Models\\TestStandModels\\ParallelModel.seq";
+
+ private string tcpServerStatus;
+ public string TcpServerStatus
+ {
+ get => tcpServerStatus;
+ set
+ {
+ Set(ref tcpServerStatus, value);
+ }
+ }
+
+ private string tcpServerInfo;
+ public string TcpServerInfo
+ {
+ get => tcpServerInfo;
+ set
+ {
+ Set(ref tcpServerInfo, value);
+ }
+ }
public ManualDebug()
{
@@ -125,8 +185,61 @@
RefreshButtons(null);
}
+ public void HideCells(bool flag)
+ {
+ CellSelectBorder.Visibility = flag ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ private List<ManualCell> Cells = new List<ManualCell>();
+ private List<StatusCell> StatusCells = new List<StatusCell>();
+
+ public void RefreshGrid(CellConfig[] cells)
+ {
+ if (cells != null)
+ {
+ int i = 0;
+ foreach (var cell in cells)
+ {
+ var mCell = new ManualCell
+ {
+ Title = cell.Name,
+ Index = cell.Index
+ };
+ mCell.SelectedChanged += MCell_SelectedChanged;
+ Cells.Add(mCell);
+ CellGrid.Children.Add(mCell);
+
+ var sCell = new StatusCell
+ {
+ Title = cell.Name,
+ Index = cell.Index
+ };
+ StatusCells.Add(sCell);
+ CellResultGrid.Children.Add(sCell);
+
+ BindingOperations.SetBinding(sCell, StatusCell.StatusProperty, new Binding(nameof(ManualCell.Selected))
+ {
+ Source = mCell,
+ Mode = BindingMode.OneWay,
+ //UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
+ Converter = CellStatusConverter.Instance
+ });
+ }
+
+ UniformGridHelper.RefreshGridInnerBorder(CellGrid);
+ UniformGridHelper.RefreshGridInnerBorder(CellResultGrid);
+ }
+ }
+
+ private void MCell_SelectedChanged(object sender, EventArgs e)
+ {
+ object[] arr = Cells.Select(c => (object)c.Selected).ToArray();
+ SetArray($"Running.CellIndex", PropertyValueTypes.PropValType_Boolean, arr);
+ }
+
private void ManualDebug_Loaded(object sender, RoutedEventArgs e)
{
+ StartTcpServer();
Task.Run(() =>
{
System.Threading.Thread.Sleep(500);
@@ -139,11 +252,117 @@
});
}
+ private int GetConfigPort()
+ {
+ try
+ {
+ var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig");
+ if (customConfig.ContainsKey("PortStart"))
+ {
+ int port = int.Parse(customConfig["PortStart"].ToString()) - 1;
+ return port;
+ }
+ else
+ {
+ return 6659;
+ }
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"读取起始端口错误: {ex.Message} {ex.StackTrace} 使用默认端口6659");
+ return 6659;
+ }
+ }
+
+ /// <summary>
+ /// 启动Tcp服务器
+ /// </summary>
+ private void StartTcpServer()
+ {
+ int port = GetConfigPort();
+ var serverConfig = new ServerConfig()
+ {
+ MaxRequestLength = 1024 * 1024,
+ Port = port
+ };
+ try
+ {
+ if (tcpServer == null)
+ {
+ tcpServer = new TcpServer();
+ tcpServer.NewRequestReceived += TcpServer_NewRequestReceived;
+ if (!tcpServer.Setup(serverConfig))
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed"));
+ return;
+ }
+ if (!tcpServer.Start())
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed"));
+ return;
+ }
+ ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}");
+ }
+ else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running)
+ {
+ if (!tcpServer.Setup(serverConfig))
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed"));
+ return;
+ }
+ if (!tcpServer.Start())
+ {
+ ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed"));
+ return;
+ }
+ ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running"));
+ AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}");
+ }
+ }
+ catch (Exception ex)
+ {
+ ChangeTcpServerStatus("ERROR", $"{ex.Message}");
+ AppendLog(LogLevel.Error, $"数据接收服务器启动错误: {ex.Message} {ex.StackTrace}");
+ tcpServer?.Stop();
+ tcpServer = null;
+ }
+ }
+
+ /// <summary>
+ /// 更新Tcp服务器状态
+ /// </summary>
+ /// <param name="status"></param>
+ /// <param name="info"></param>
+ private void ChangeTcpServerStatus(string status, string info)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ TcpServerStatus = status;
+ TcpServerInfo = info;
+ });
+ }
+
+ private void TcpServer_NewRequestReceived(TcpSession session, SuperSocket.SocketBase.Protocol.StringRequestInfo requestInfo)
+ {
+ string msg = requestInfo.Body;
+ string[] datas_10015 = msg.Split(';');
+ //int cellIndex = int.Parse(datas_10015[1]);
+
+ string strs = datas_10015[2];
+ string[] arr = strs.Split(',');
+ Dispatcher.Invoke(() =>
+ {
+ AddTestData(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7]);
+ });
+ }
+
public void RefreshButtons(List<BtnConfig> settings)
{
if (settings == null) return;
- for (int i = 0; i < settings.Count; i++) {
+ for (int i = 0; i < settings.Count; i++)
+ {
var setting = settings[i];
var margin = new Thickness(0, 0, 20, 0);
@@ -196,6 +415,35 @@
}
}
+ private void SetArray(string name, PropertyValueTypes type, object[] arr)
+ {
+ try
+ {
+ var context = ((TeststandWrapper)executionWrapper).context;
+ context.FileGlobals.DeleteSubProperty(name, 2);
+
+ context.FileGlobals.NewSubProperty(name, type, true, null, 1);
+ context.FileGlobals.GetPropertyObject(name, 0).SetNumElements(arr.Length);
+
+ for (int i = 0; i < arr.Length; i++)
+ {
+ if (type == PropertyValueTypes.PropValType_Boolean)
+ {
+ context.FileGlobals.SetValBoolean($"{name}[{i}]", 0, arr[i] == null ? false : (bool)arr[i]);
+ }
+ else
+ {
+ context.FileGlobals.SetValString($"{name}[{i}]", 0, arr[i]?.ToString());
+ }
+ // (type == PropertyValueTypes.PropValType_String)
+ }
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"写数组 {name} 错误: {ex.Message} {ex.StackTrace}");
+ }
+ }
+
private void Wrapper_UIMessageEvent(object sender, UIMessageEvent e)
{
try
@@ -207,15 +455,35 @@
string key = $"Running.OvenName";
executionWrapper?.SetFileGlobalValue(key, 1, OvenName);
- key = $"Running.CellName";
- executionWrapper?.SetFileGlobalValue(key, 1, CellName);
-
key = $"Running.OvenIndex";
executionWrapper?.SetFileGlobalValue(key, 1, OvenIndex);
- key = $"Running.CellIndex";
- executionWrapper?.SetFileGlobalValue(key, 1, CellIndex);
+ //key = $"Running.CellIndex";
+ //executionWrapper?.SetFileGlobalValue(key, 1, CellIndex);
+ if (CellIndex >= 0)
+ {
+ key = $"Running.CellIndex";
+ object[] arr = new object[CellCount];
+ arr[CellIndex] = true;
+ SetArray(key, PropertyValueTypes.PropValType_Boolean, arr);
+ key = $"Running.CellName";
+ string[] names = new string[CellCount];
+ names[CellIndex] = CellName;
+ SetArray(key, PropertyValueTypes.PropValType_String, names);
+ }
+ else
+ {
+ string[] names = Cells.Select(c => c.Title).ToArray();
+ SetArray($"Running.CellName", PropertyValueTypes.PropValType_String, names);
+ }
+
+
+ Dispatcher.Invoke(() =>
+ {
+ CellGrid.IsEnabled = true;
+ AppendLog(LogLevel.Debug, "Context初始化完成");
+ });
return;
}
if (e.EventCode == 10001)
@@ -247,14 +515,26 @@
});
return;
}
+ if (e.EventCode == 10004)
+ {
+ int index = Convert.ToInt32(e.NumberData);
+ //CellStatus status = (CellStatus)Enum.Parse(typeof(CellStatus), e.StrData);
+ CellStatus status = (CellStatus)int.Parse(e.StrData);
+ Dispatcher.BeginInvoke(() =>
+ {
+ if (index < StatusCells.Count)
+ {
+ StatusCells[index].SetCurrentValue(StatusCell.StatusProperty, status);
+ }
+ });
+ return;
+ }
}
catch (Exception ex)
{
AppendLog(LogLevel.Error, $"处理UI信息失败 {ex.Message} {ex.StackTrace}");
}
}
-
-
private void Btn_BtnClick(object sender, RoutedEventArgs e)
{
@@ -391,12 +671,19 @@
{
executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true);
executionWrapper?.WaitForExit();
+ executionWrapper?.Dispose();
+ tcpServer?.Stop();
}
catch (Exception ex)
{
}
}
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ StartTcpServer();
+ }
}
public class TestDataItem : INotifyPropertyChanged
diff --git a/Util.ManualDebug/ManualDebugWindow.xaml b/Util.ManualDebug/ManualDebugWindow.xaml
new file mode 100644
index 0000000..bf32571
--- /dev/null
+++ b/Util.ManualDebug/ManualDebugWindow.xaml
@@ -0,0 +1,23 @@
+锘�<pu:WindowX x:Class="Util.ManualDebug.ManualDebugWindow"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ mc:Ignorable="d"
+ Title="ManualDebugWindow"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ xmlns:local="clr-namespace:Util.ManualDebug"
+ Style="{StaticResource ModalWindow}"
+ Closing="Window_Closing"
+ WindowState="Maximized"
+ WindowStartupLocation="CenterScreen"
+ pu:WindowXModalDialog.Buttons="{x:Null}"
+ Height="1000" Width="900">
+ <Grid x:Name="sb">
+ <local:ManualDebug OvenName="{Binding OvenName,Mode=OneWay}"
+ CellCount="{Binding CellCount,Mode=OneWay}"
+ CellName="{Binding CellName,Mode=OneWay}"
+ OvenIndex="{Binding OvenIndex,Mode=OneWay}"
+ CellIndex="{Binding CellIndex,Mode=OneWay}"/>
+ </Grid>
+</pu:WindowX>
diff --git a/Util.ManualDebug/ManualDebugWindow.xaml.cs b/Util.ManualDebug/ManualDebugWindow.xaml.cs
new file mode 100644
index 0000000..0864671
--- /dev/null
+++ b/Util.ManualDebug/ManualDebugWindow.xaml.cs
@@ -0,0 +1,114 @@
+锘縰sing HMI.Util;
+using Panuon.WPF.UI;
+using System.ComponentModel;
+using System.IO;
+using System.Windows;
+using Util.ManualDebug.Model;
+
+namespace Util.ManualDebug
+{
+ /// <summary>
+ /// ManualDebugWindow.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class ManualDebugWindow : WindowX
+ {
+
+ private ManualDebug debug;
+
+ public ManualDebugWindow(string seqPath, string modelPath, bool isMuilt) : this(seqPath, modelPath, isMuilt, null, 1)
+ {
+ }
+
+ public ManualDebugWindow(string seqPath, string modelPath, bool isMuilt, CellConfig[] cells, int col)
+ {
+ DataContext = this;
+ InitializeComponent();
+ debug = (ManualDebug)sb.Children[0];
+ ReadBtns();
+ if (!isMuilt)
+ {
+ debug.HideCells(true);
+ }
+ //List<BtnConfig> settings = new List<BtnConfig> { new BtnConfig { Title = "涓婄數婵�娲�", Name = "Active" } };
+ debug.seqPath = seqPath;
+ debug.modelPath = modelPath;
+ debug.CellGrid.Columns = col;
+ debug.CellResultGrid.Columns = col;
+ debug.RefreshGrid(cells);
+ }
+
+ private string ovenName;
+ public string OvenName
+ {
+ get => ovenName;
+ set
+ {
+ Set(ref ovenName, value);
+ }
+ }
+
+ private string cellName;
+ public string CellName
+ {
+ get => cellName;
+ set
+ {
+ Set(ref cellName, value);
+ }
+ }
+
+ private int ovenIndex;
+ public int OvenIndex
+ {
+ get => ovenIndex;
+ set
+ {
+ Set(ref ovenIndex, value);
+ }
+ }
+
+ private int cellIndex = -1;
+ public int CellIndex
+ {
+ get => cellIndex;
+ set
+ {
+ Set(ref cellIndex, value);
+ }
+ }
+
+ private int cellCount = 0;
+ public int CellCount
+ {
+ get => cellCount;
+ set
+ {
+ Set(ref cellCount, value);
+ }
+ }
+
+ private void ReadBtns()
+ {
+ string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.ini");
+ var map = IniFileHelper.GetKeyValuesBySection(path, "Btns");
+ List<BtnConfig> settings = new List<BtnConfig>();
+ foreach (var kv in map)
+ {
+ settings.Add(new BtnConfig { Title = kv.Key, Name = kv.Value });
+ }
+ debug.RefreshButtons(settings);
+ }
+
+ private void Window_Closing(object sender, CancelEventArgs e)
+ {
+ if (MessageBox.Show("纭畾瑕佸叧闂悧锛�", "鎻愮ず", MessageBoxButton.OKCancel) == MessageBoxResult.OK)
+ {
+ debug.Stop();
+ }
+ else
+ {
+ e.Cancel = true;
+ }
+ }
+ }
+}
diff --git a/Util.ManualDebug/Model/CellConfig.cs b/Util.ManualDebug/Model/CellConfig.cs
new file mode 100644
index 0000000..1b6cbb9
--- /dev/null
+++ b/Util.ManualDebug/Model/CellConfig.cs
@@ -0,0 +1,14 @@
+锘縰sing System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Util.ManualDebug.Model
+{
+ public class CellConfig
+ {
+ public string Name { get; set; }
+ public int Index { get; set; }
+ }
+}
diff --git a/Util.ManualDebug/Model/TcpServer.cs b/Util.ManualDebug/Model/TcpServer.cs
new file mode 100644
index 0000000..03e1cf4
--- /dev/null
+++ b/Util.ManualDebug/Model/TcpServer.cs
@@ -0,0 +1,57 @@
+锘縰sing SuperSocket.SocketBase;
+using SuperSocket.SocketBase.Protocol;
+using System.Text;
+
+namespace Util.ManualDebug.Model
+{
+ public class DTReceiveFilter : ReceiveFilterBase<StringRequestInfo>
+ {
+ public override StringRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest)
+ {
+ rest = 0;
+ int i = offset;
+ List<byte> bytes = new List<byte>();
+ while (i < readBuffer.Length)
+ {
+ byte b = readBuffer[i++];
+ if (b == '\n')
+ {
+ break;
+ }
+ bytes.Add(b);
+ }
+ byte[] arr = bytes.ToArray();
+ return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), Encoding.UTF8.GetString(arr), null);//sb.ToString()
+ }
+ }
+
+ public class StringParser : IRequestInfoParser<StringRequestInfo>
+ {
+
+ public StringRequestInfo ParseRequestInfo(string source)
+ {
+ return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), source, null);
+ }
+ }
+
+ //public class TcpServer : AppServer<TcpSession>
+ //{
+ // public TcpServer() : base(new DefaultReceiveFilterFactory<DTReceiveFilter, StringRequestInfo>())
+ // {
+
+ // }
+ //}
+
+ public class TcpServer : AppServer<TcpSession>
+ {
+ public TcpServer() : base(new TerminatorReceiveFilterFactory("\n", Encoding.UTF8, new StringParser()))
+ {
+
+ }
+ }
+
+ public class TcpSession : AppSession<TcpSession, StringRequestInfo>
+ {
+
+ }
+}
diff --git a/Util.ManualDebug/OldManualDebug.xaml b/Util.ManualDebug/OldManualDebug.xaml
new file mode 100644
index 0000000..791cd4c
--- /dev/null
+++ b/Util.ManualDebug/OldManualDebug.xaml
@@ -0,0 +1,133 @@
+<UserControl x:Class="Util.ManualDebug.OldManualDebug"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:local="clr-namespace:Util.ManualDebug"
+ mc:Ignorable="d"
+ x:Name="self"
+ MinHeight="400" MinWidth="500">
+
+ <UserControl.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <ResourceDictionary Source="/Util.ManualDebug;component/ManualDebugStyle.xaml"/>
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </UserControl.Resources>
+
+ <Grid>
+ <Grid Margin="10">
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="5"/>
+ <RowDefinition Height="*"/>
+ <RowDefinition Height="5"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+ <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost>
+
+ <!-- 鎸夐挳鍜岀姸鎬佸尯鍩� -->
+ <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}"
+ BorderBrush="{StaticResource BorderBrush}"
+ BorderThickness="1" CornerRadius="4" Padding="15">
+ <StackPanel>
+ <TextBlock Text="鎵嬪姩璋冭瘯鎺у埗" FontSize="14" FontWeight="SemiBold"
+ Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/>
+
+ <WrapPanel x:Name="btnPanel">
+
+ </WrapPanel>
+ </StackPanel>
+ </Border>
+
+ <!-- 鍒嗛殧鏉�1 -->
+ <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
+ Background="Transparent" Cursor="SizeNS"/>
+
+ <!-- TX/RX 鏁版嵁鍖哄煙 -->
+ <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}"
+ BorderBrush="{StaticResource BorderBrush}"
+ BorderThickness="1" CornerRadius="4" Padding="10">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10">
+ <TextBlock Text="娴嬭瘯鏁版嵁" FontSize="14" FontWeight="SemiBold"
+ Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
+ <Button Content="娓呯┖" Style="{StaticResource ActionButtonStyle}"
+ Margin="15,0,0,0" Click="OnClearClick"/>
+ </StackPanel>
+
+ <DataGrid x:Name="DataGridComm" Grid.Row="1"
+ ItemsSource="{Binding TestData,ElementName=self}">
+ <DataGrid.Columns>
+ <DataGridTextColumn Header="娴嬭瘯椤�" Binding="{Binding Name}" Width="120" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鏀�" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鍙�" Binding="{Binding Send}" Width="*" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鍗曚綅" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/>
+ <DataGridTextColumn Header="涓婇檺" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/>
+ <DataGridTextColumn Header="涓嬮檺" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/>
+ <DataGridTextColumn Header="缁撴灉" Binding="{Binding Result}" Width="70" IsReadOnly="True"/>
+ <DataGridTextColumn Header="鏃堕棿" Binding="{Binding Time}" Width="90" IsReadOnly="True"/>
+ </DataGrid.Columns>
+ <DataGrid.RowStyle>
+ <Style TargetType="DataGridRow">
+ <Style.Triggers>
+ <DataTrigger Value="True">
+ <DataTrigger.Binding>
+ <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}">
+ <Binding Path="Result"/>
+ </MultiBinding>
+ </DataTrigger.Binding>
+ <Setter Property="Background" Value="#FFE0E0"/>
+ <Setter Property="Foreground" Value="#C53030"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </DataGrid.RowStyle>
+ </DataGrid>
+ </Grid>
+ </Border>
+
+ <!-- 鍒嗛殧鏉�2 -->
+ <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch"
+ Background="Transparent" Cursor="SizeNS"/>
+
+ <!-- 鏃ュ織鍖哄煙 -->
+ <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}"
+ BorderBrush="{StaticResource BorderBrush}"
+ BorderThickness="1" CornerRadius="4" Padding="10">
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition Height="Auto"/>
+ <RowDefinition Height="*"/>
+ </Grid.RowDefinitions>
+
+ <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8">
+ <TextBlock Text="鏃ュ織" FontSize="14" FontWeight="SemiBold"
+ Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/>
+ <Button Content="娓呯┖鏃ュ織" Style="{StaticResource ActionButtonStyle}"
+ Margin="15,0,0,0" Click="OnClearLogClick"/>
+ </StackPanel>
+
+ <RichTextBox x:Name="LogRichTextBox" Grid.Row="1"
+ IsReadOnly="True"
+ VerticalScrollBarVisibility="Auto"
+ FontFamily="Consolas"
+ FontSize="11"
+ BorderThickness="1"
+ BorderBrush="Black"
+ Padding="4"/>
+ </Grid>
+ </Border>
+ </Grid>
+ <Border Background="#AAFFFFFF" x:Name="mask">
+ <TextBlock Text="鍒濆鍖栦腑..."
+ FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+ </Border>
+ </Grid>
+</UserControl>
diff --git a/Util.ManualDebug/OldManualDebug.xaml.cs b/Util.ManualDebug/OldManualDebug.xaml.cs
new file mode 100644
index 0000000..2fc4f5c
--- /dev/null
+++ b/Util.ManualDebug/OldManualDebug.xaml.cs
@@ -0,0 +1,368 @@
+using Plugin.TsSupport;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Media;
+
+namespace Util.ManualDebug
+{
+ public partial class OldManualDebug : UserControl
+ {
+ public static readonly DependencyProperty OvenNameProperty = DependencyProperty.Register(
+ "OvenName",
+ typeof(string),
+ typeof(OldManualDebug),
+ new PropertyMetadata(""));
+
+ public static readonly DependencyProperty CellNameProperty = DependencyProperty.Register(
+ "CellName",
+ typeof(string),
+ typeof(OldManualDebug),
+ new PropertyMetadata(""));
+
+ public static readonly DependencyProperty OvenIndexProperty = DependencyProperty.Register(
+ "OvenIndex",
+ typeof(int),
+ typeof(OldManualDebug),
+ new PropertyMetadata(0));
+
+ public static readonly DependencyProperty CellIndexProperty = DependencyProperty.Register(
+ "CellIndex",
+ typeof(int),
+ typeof(OldManualDebug),
+ new PropertyMetadata(0));
+
+ public string OvenName
+ {
+ get { return (string)GetValue(OvenNameProperty); }
+ set
+ {
+ SetValue(OvenNameProperty, value);
+ }
+ }
+
+ public string CellName
+ {
+ get { return (string)GetValue(CellNameProperty); }
+ set { SetValue(CellNameProperty, value); }
+ }
+
+ public int OvenIndex
+ {
+ get { return (int)GetValue(OvenIndexProperty); }
+ set { SetValue(OvenIndexProperty, value); }
+ }
+
+ public int CellIndex
+ {
+ get { return (int)GetValue(CellIndexProperty); }
+ set { SetValue(CellIndexProperty, value); }
+ }
+
+ public ObservableCollection<TestDataItem> TestData { get; private set; } = new ObservableCollection<TestDataItem>();
+
+ private Dictionary<string, StatusBtn> btnCache = new Dictionary<string, StatusBtn>();
+
+ // 日志级别颜色
+ private readonly SolidColorBrush _debugBrush;
+ private readonly SolidColorBrush _infoBrush;
+ private readonly SolidColorBrush _warnBrush;
+ private readonly SolidColorBrush _errorBrush;
+
+ private ITestExecutionWrapper executionWrapper;
+
+ public string seqPath { get; set; }
+ public string modelPath { get; set; } = "C:\\Program Files\\National Instruments\\TestStand 2020\\Components\\Models\\TestStandModels\\ParallelModel.seq";
+
+ public OldManualDebug()
+ {
+ _debugBrush = new SolidColorBrush(Color.FromRgb(0x71, 0x80, 0x8A)); // 灰色
+ _infoBrush = new SolidColorBrush(Colors.Black);
+ _warnBrush = new SolidColorBrush(Color.FromRgb(0xD6, 0x9E, 0x2E)); // 橙黄色
+ _errorBrush = new SolidColorBrush(Color.FromRgb(0xC5, 0x30, 0x30)); // 红色
+ this.Loaded += ManualDebug_Loaded;
+ InitializeComponent();
+ RefreshButtons(null);
+ }
+
+ private void ManualDebug_Loaded(object sender, RoutedEventArgs e)
+ {
+ Task.Run(() =>
+ {
+ System.Threading.Thread.Sleep(500);
+ Dispatcher.BeginInvoke(() =>
+ {
+ InitWrapper();
+ StartSeq();
+ mask.Visibility = Visibility.Collapsed;
+ });
+ });
+ }
+
+ public void RefreshButtons(List<BtnConfig> settings)
+ {
+ if (settings == null) return;
+
+ for (int i = 0; i < settings.Count; i++)
+ {
+ var setting = settings[i];
+
+ var margin = new Thickness(0, 0, 20, 0);
+ if (i == settings.Count - 1)
+ {
+ margin = new Thickness(0);
+ }
+
+ var btn = new StatusBtn
+ {
+ Title = setting.Title,
+ Tag = setting,
+ Margin = margin
+ };
+ btn.BtnClick += Btn_BtnClick;
+ btnCache[setting.Name] = btn;
+ btnPanel.Children.Add(btn);
+ }
+ }
+
+ public void StartSeq()
+ {
+ try
+ {
+ executionWrapper?.StartSequence(seqPath, modelPath);
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"启动测试序列失败 {ex.Message} {ex.StackTrace}");
+ }
+ }
+
+ private void InitWrapper()
+ {
+ try
+ {
+ var wrapper = new TeststandWrapper();
+ var AxApplicationMgr = wrapper.AxApplicationMgr;
+ ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).BeginInit();
+ host.Child = AxApplicationMgr;
+ ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).EndInit();
+ wrapper.Init();
+ executionWrapper = wrapper;
+
+ wrapper.UIMessageEvent += Wrapper_UIMessageEvent;
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"初始化Teststand {ex.Message} {ex.StackTrace}");
+ }
+ }
+
+ private void Wrapper_UIMessageEvent(object sender, UIMessageEvent e)
+ {
+ try
+ {
+ if (e.EventCode == 10000)
+ {
+ executionWrapper?.SetContext(e.Data);
+
+ string key = $"Running.OvenName";
+ executionWrapper?.SetFileGlobalValue(key, 1, OvenName);
+
+ key = $"Running.CellName";
+ executionWrapper?.SetFileGlobalValue(key, 1, CellName);
+
+ key = $"Running.OvenIndex";
+ executionWrapper?.SetFileGlobalValue(key, 1, OvenIndex);
+
+ key = $"Running.CellIndex";
+ executionWrapper?.SetFileGlobalValue(key, 1, CellIndex);
+
+ return;
+ }
+ if (e.EventCode == 10001)
+ {
+ string[] arr = e.StrData.Split(',');
+ Dispatcher.BeginInvoke(() =>
+ {
+ AddTestData(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7]);
+ });
+ return;
+ }
+ if (e.EventCode == 10002)
+ {
+ string[] arr = e.StrData.Split(',');
+ LogLevel level = (LogLevel)int.Parse(arr[0]);
+ Dispatcher.BeginInvoke(() =>
+ {
+ AppendLog(level, arr[1]);
+ });
+ return;
+ }
+ if (e.EventCode == 10003)
+ {
+ string[] arr = e.StrData.Split(',');
+ DebugStatus status = (DebugStatus)int.Parse(arr[1]);
+ Dispatcher.BeginInvoke(() =>
+ {
+ SetStatus(arr[0], status);
+ });
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"处理UI信息失败 {ex.Message} {ex.StackTrace}");
+ }
+ }
+
+
+
+ private void Btn_BtnClick(object sender, RoutedEventArgs e)
+ {
+ var btn = (StatusBtn)sender;
+ BtnConfig config = (BtnConfig)btn.Tag;
+ StartSingleTest(config.Name);
+ }
+
+ private void StartSingleTest(string name)
+ {
+ try
+ {
+ string key = $"Running.ActionName";
+ executionWrapper?.SetFileGlobalValue(key, 1, name);
+ }
+ catch (Exception ex)
+ {
+ AppendLog(LogLevel.Error, $"启动功能失败 {ex.Message} {ex.StackTrace}");
+ }
+ }
+
+ // 设置状态显示
+ private void SetStatus(string labelName, DebugStatus status)
+ {
+ if (btnCache.ContainsKey(labelName))
+ {
+ var btn = btnCache[labelName];
+ btn.Status = status;
+ }
+ }
+
+ // 重置状态显示
+ private void ResetStatus(string labelName)
+ {
+ SetStatus(labelName, DebugStatus.Idle);
+ }
+
+ // 设置测试中状态并禁用按钮
+ private void SetTestingStatus(string labelName, Button button)
+ {
+ SetStatus(labelName, DebugStatus.Testing);
+ button.IsEnabled = false;
+ }
+
+ private void OnClearClick(object sender, RoutedEventArgs e)
+ {
+ TestData.Clear();
+ }
+
+ private void OnClearLogClick(object sender, RoutedEventArgs e)
+ {
+ LogRichTextBox.Document.Blocks.Clear();
+ }
+
+ private void AddTestData(string name, string send, string receive, string unit, string upper, string lower, string result, string time)
+ {
+ TestData.Add(new TestDataItem
+ {
+ Name = name,
+ Send = send,
+ Receive = receive,
+ Unit = unit,
+ UpperLimit = upper,
+ LowerLimit = lower,
+ Result = result,
+ Time = time
+ });
+
+ if (DataGridComm.Items.Count > 0)
+ {
+ DataGridComm.ScrollIntoView(DataGridComm.Items[DataGridComm.Items.Count - 1]);
+ }
+ }
+
+ // 获取日志级别对应的颜色
+ private Brush GetLogLevelBrush(LogLevel level)
+ {
+ switch (level)
+ {
+ case LogLevel.Debug:
+ return _debugBrush;
+ case LogLevel.Info:
+ return _infoBrush;
+ case LogLevel.Warn:
+ return _warnBrush;
+ case LogLevel.Error:
+ return _errorBrush;
+ default:
+ return _infoBrush;
+ }
+ }
+
+ // 添加带级别的日志
+ private void AppendLog(LogLevel level, string message)
+ {
+ Dispatcher.BeginInvoke(() =>
+ {
+ string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
+ string levelStr = level.ToString().ToUpper();
+
+ Paragraph? paragraph = LogRichTextBox.Document.Blocks.LastBlock as Paragraph;
+ if (paragraph == null)
+ {
+ paragraph = new Paragraph();
+ LogRichTextBox.Document.Blocks.Add(paragraph);
+ }
+
+ // 时间戳(灰色)
+ Run timestampRun = new Run("[" + timestamp + "] ") { Foreground = _debugBrush };
+ paragraph.Inlines.Add(timestampRun);
+
+ // 级别标签
+ Brush levelBrush = GetLogLevelBrush(level);
+ Run levelRun = new Run("[" + levelStr + "] ") { Foreground = levelBrush, FontWeight = FontWeights.SemiBold };
+ paragraph.Inlines.Add(levelRun);
+
+ // 消息内容
+ Run messageRun = new Run(message + "\n") { Foreground = levelBrush };
+ paragraph.Inlines.Add(messageRun);
+
+ LogRichTextBox.ScrollToEnd();
+ });
+ }
+
+ // 兼容旧调用方式,默认 Info 级别
+ private void AppendLog(string message)
+ {
+ AppendLog(LogLevel.Info, message);
+ }
+
+ public void Stop()
+ {
+ try
+ {
+ executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true);
+ executionWrapper?.WaitForExit();
+ executionWrapper?.Dispose();
+ }
+ catch (Exception ex)
+ {
+
+ }
+ }
+ }
+
+}
diff --git a/Util.ManualDebug/StatusBtn.xaml b/Util.ManualDebug/StatusBtn.xaml
index 7b96029..9321114 100644
--- a/Util.ManualDebug/StatusBtn.xaml
+++ b/Util.ManualDebug/StatusBtn.xaml
@@ -1,9 +1,9 @@
-锘�<UserControl x:Class="Util.SB.StatusBtn"
+锘�<UserControl x:Class="Util.ManualDebug.StatusBtn"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:local="clr-namespace:Util.SB"
+ xmlns:local="clr-namespace:Util.ManualDebug"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
diff --git a/Util.ManualDebug/StatusBtn.xaml.cs b/Util.ManualDebug/StatusBtn.xaml.cs
index 11c2783..ce40f51 100644
--- a/Util.ManualDebug/StatusBtn.xaml.cs
+++ b/Util.ManualDebug/StatusBtn.xaml.cs
@@ -1,8 +1,9 @@
-锘縰sing CommunityToolkit.Mvvm.ComponentModel;
+锘縰sing System.ComponentModel;
+using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
-namespace Util.SB
+namespace Util.ManualDebug
{
// 鐘舵�佹灇涓�
public enum DebugStatus
@@ -15,9 +16,26 @@
/// <summary>
/// StatusBtn.xaml 鐨勪氦浜掗�昏緫
/// </summary>
- [ObservableObject]
- public partial class StatusBtn : UserControl
+ public partial class StatusBtn : UserControl, INotifyPropertyChanged
{
+ #region ...
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void Set([CallerMemberName] string name = null)
+ {
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
+ {
+ old = @new;
+ if (propertyName != null)
+ {
+ Set(propertyName);
+ }
+ }
+ #endregion
public event RoutedEventHandler BtnClick;
public StatusBtn()
@@ -26,11 +44,25 @@
InitializeComponent();
}
- [ObservableProperty]
private string title = string.Empty;
+ public string Title
+ {
+ get => title;
+ set
+ {
+ Set(ref title, value);
+ }
+ }
- [ObservableProperty]
private DebugStatus status = DebugStatus.Idle;
+ public DebugStatus Status
+ {
+ get => status;
+ set
+ {
+ Set(ref status, value);
+ }
+ }
private void Button_Click(object sender, RoutedEventArgs e)
{
diff --git a/Util.ManualDebug/StatusCell.xaml b/Util.ManualDebug/StatusCell.xaml
new file mode 100644
index 0000000..30d3365
--- /dev/null
+++ b/Util.ManualDebug/StatusCell.xaml
@@ -0,0 +1,39 @@
+锘�<UserControl x:Class="Util.ManualDebug.StatusCell"
+ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:local="clr-namespace:Util.ManualDebug"
+ xmlns:pu="https://opensource.panuon.com/wpf-ui"
+ mc:Ignorable="d" Padding="5" MaxHeight="80"
+ d:DesignHeight="450" d:DesignWidth="800">
+ <UserControl.Style>
+ <Style TargetType="UserControl">
+ <Style.Triggers>
+ <DataTrigger Binding="{Binding Status}" Value="{x:Static local:CellStatus.Closed}">
+ <Setter Property="Background" Value="Gray"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="{x:Static local:CellStatus.OK}">
+ <Setter Property="Background" Value="#2D8659"/>
+ <Setter Property="Foreground" Value="#FFF"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="{x:Static local:CellStatus.NG}">
+ <Setter Property="Background" Value="#C53030"/>
+ <Setter Property="Foreground" Value="#FFF"/>
+ </DataTrigger>
+ <DataTrigger Binding="{Binding Status}" Value="{x:Static local:CellStatus.Testing}">
+ <Setter Property="Background" Value="#3182CE"/>
+ <Setter Property="Foreground" Value="#FFF"/>
+ </DataTrigger>
+ </Style.Triggers>
+ </Style>
+ </UserControl.Style>
+ <Grid>
+ <Grid.RowDefinitions>
+ <RowDefinition/>
+ <RowDefinition/>
+ </Grid.RowDefinitions>
+ <TextBlock FontSize="20" FontWeight="Bold" Text="{Binding Title}"/>
+ <TextBlock Grid.Row="1" FontSize="18" Text="{Binding Status}"/>
+ </Grid>
+</UserControl>
diff --git a/Util.ManualDebug/StatusCell.xaml.cs b/Util.ManualDebug/StatusCell.xaml.cs
new file mode 100644
index 0000000..2649f72
--- /dev/null
+++ b/Util.ManualDebug/StatusCell.xaml.cs
@@ -0,0 +1,104 @@
+锘縰sing System.ComponentModel;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+
+namespace Util.ManualDebug
+{
+ public enum CellStatus
+ {
+ Testing = 0,
+ OK = 1,
+ NG = 2,
+ Closed = 99,
+ Open = 100,
+ Waiting = 101
+ }
+
+ public class CellStatusConverter : IValueConverter
+ {
+ public static CellStatusConverter Instance = new CellStatusConverter();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is bool b)
+ {
+ return b ? CellStatus.Open : CellStatus.Closed;
+ }
+ return CellStatus.Closed;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// ManualCell.xaml 鐨勪氦浜掗�昏緫
+ /// </summary>
+ public partial class StatusCell : UserControl, INotifyPropertyChanged
+ {
+ #region ...
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void Set([CallerMemberName] string name = null)
+ {
+
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+
+ protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null)
+ {
+ old = @new;
+ if (propertyName != null)
+ {
+ Set(propertyName);
+ }
+ }
+ #endregion
+ private string title;
+ public string Title
+ {
+ get => title;
+ set
+ {
+ Set(ref title, value);
+ }
+ }
+
+ public static readonly DependencyProperty StatusProperty = DependencyProperty.Register(
+ "Status",
+ typeof(CellStatus),
+ typeof(StatusCell),
+ new PropertyMetadata(CellStatus.Closed));
+
+ public CellStatus Status
+ {
+ get => (CellStatus)GetValue(StatusProperty);
+ set
+ {
+ SetValue(StatusProperty, value);
+ }
+ }
+
+
+ private int index;
+ public int Index
+ {
+ get => index;
+ set
+ {
+ Set(ref index, value);
+ }
+ }
+
+ public StatusCell()
+ {
+ this.DataContext = this;
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Util.ManualDebug/Util.ManualDebug.csproj b/Util.ManualDebug/Util.ManualDebug.csproj
index fcf9c6e..13a1681 100644
--- a/Util.ManualDebug/Util.ManualDebug.csproj
+++ b/Util.ManualDebug/Util.ManualDebug.csproj
@@ -11,11 +11,15 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
+ <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" />
+ <PackageReference Include="SuperSocket" Version="1.6.6.1" />
+ <PackageReference Include="SuperSocket.Engine" Version="1.6.6.1" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" />
+ <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" />
<ProjectReference Include="..\Plugin.TsSupport\Plugin.TsSupport.csproj" />
</ItemGroup>
@@ -30,4 +34,13 @@
<Reference Include="WindowsFormsIntegration" />
</ItemGroup>
+ <ItemGroup>
+ <Compile Update="ManualCell.xaml.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Update="StatusCell.xaml.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ </ItemGroup>
+
</Project>
diff --git a/WPFProject/App.config b/WPFProject/App.config
index 0985874..6580438 100644
--- a/WPFProject/App.config
+++ b/WPFProject/App.config
@@ -3,8 +3,7 @@
<configSections>
<section name="TestResultConfig" type="System.Configuration.SingleTagSectionHandler" />
</configSections>
- <TestResultConfig Row="2" Column="2" PortStart="6660" PositionColumn="4" PositionCount="20" NameStr="A,B,C,D,E,F,G"
- CodeParserType="Model"/>
+ <TestResultConfig Row="2" Column="2" PortStart="6660" PositionColumn="4" PositionCount="20" NameStr="A,B,C,D,E,F,G" CodeParserType="Model" EngineType="Tap" />
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
diff --git a/WPFProject/App.xaml b/WPFProject/App.xaml
index 289fbe1..484c7c0 100644
--- a/WPFProject/App.xaml
+++ b/WPFProject/App.xaml
@@ -19,7 +19,12 @@
<ResourceDictionary Source="/HMI.UI;component/Style/PanuonResource.xaml"/>
<ResourceDictionary Source="/HMI.UI;component/Style/MsgBoxStyle.xaml"/>
<ResourceDictionary Source="/HMI.Base;component/MaskControl.xaml"/>
+ <ResourceDictionary Source="/UILib;component/Styles.xaml"/>
+ <ResourceDictionary Source="/SeqEditor;component/UIStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
+ <FontFamily x:Key="iconfont">
+ /HMI.UI;component/Resources/Font/#iconfont
+ </FontFamily>
<sys:Double x:Key="nanD">NaN</sys:Double>
<Style TargetType="{x:Type pu:FormGroup}" BasedOn="{StaticResource InputFormGroupStyle}"></Style>
<uic:PathImageConverter x:Key="path2Image"/>
diff --git a/WPFProject/App.xaml.cs b/WPFProject/App.xaml.cs
index f20a0c6..4a335ed 100644
--- a/WPFProject/App.xaml.cs
+++ b/WPFProject/App.xaml.cs
@@ -3,7 +3,9 @@
using HMI.UI.Util;
using log4net;
using Microsoft.Win32;
+using OpenTap.Addin.Util;
using Panuon.WPF.UI;
+using Plugin.TsSupport;
using System;
using System.Diagnostics;
using System.IO;
@@ -96,6 +98,7 @@
mutex = new Mutex(true, "TestPlatform_OnlyRun_CRNS__");
if (mutex.WaitOne(0, false))//&& process == null
{
+ RegUtil.Register(new Net47Getter());
WriteRegister();
try
{
diff --git a/WPFProject/BuildCmd.bat b/WPFProject/BuildCmd.bat
index 2c58c66..948bdf0 100644
--- a/WPFProject/BuildCmd.bat
+++ b/WPFProject/BuildCmd.bat
@@ -51,11 +51,16 @@
move /y "$(TargeDir)Util.ManualDebug.dll" $(TargeDir)Plugin\Util.ManualDebug.dll"
move /y "$(TargeDir)Plugin.QualityReport.dll" $(TargeDir)Plugin\Plugin.QualityReport.dll"
move /y "$(TargeDir)Plugin.BurnIn.dll" $(TargeDir)Plugin\Plugin.BurnIn.dll"
+move /y "$(TargeDir)Plugin.StandardTest.dll" $(TargeDir)Plugin\Plugin.StandardTest.dll"
+move /y "$(TargeDir)Plugin.TestPlanEditor.dll" $(TargeDir)Plugin\Plugin.TestPlanEditor.dll"
move /y "$(TargeDir)AddInPlugin.dll" "$(TargetDir)OpenTap\AddInPlugin.dll"
move /y "$(TargeDir)OpenTap.dll" "$(TargetDir)OpenTap\OpenTap.dll"
move /y "$(TargeDir)OpenTap.Plugins.BasicSteps.dll" "$(TargetDir)OpenTap\OpenTap.Plugins.BasicSteps.dll"
move /y "$(TargeDir)HMI.TapApi.dll" "$(TargetDir)OpenTap\HMI.TapApi.dll"
+move /y "$(TargeDir)ViHelper.dll" "$(TargetDir)OpenTap\ViHelper.dll"
+move /y "$(TargeDir)UtilLib.dll" "$(TargetDir)OpenTap\UtilLib.dll"
+move /y "$(TargeDir)LabViewRunner.dll" "$(TargetDir)OpenTap\LabViewRunner.dll"
setlocal enabledelayedexpansion
@@ -72,7 +77,7 @@
)
for /d %%D in ("%source_folder%\*") do (
- if /I not "%%~nxD"=="Api" if /I not "%%~nxD"=="Config" if /I not "%%~nxD"=="Icon" if /I not "%%~nxD"=="Lang" if /I not "%%~nxD"=="Libs" if /I not "%%~nxD"=="Log" if /I not "%%~nxD"=="Plugin" if /I not "%%~nxD"=="TestPlatform.exe.WebView2" if /I not "%%~nxD"=="OpenTap" (
+ if /I not "%%~nxD"=="Api" if /I not "%%~nxD"=="Config" if /I not "%%~nxD"=="Icon" if /I not "%%~nxD"=="Lang" if /I not "%%~nxD"=="Libs" if /I not "%%~nxD"=="Log" if /I not "%%~nxD"=="Plugin" if /I not "%%~nxD"=="TestPlatform.exe.WebView2" if /I not "%%~nxD"=="OpenTap" if /I not "%%~nxD"=="Settings" if /I not "%%~nxD"=="Report" (
move /Y "%%D" "%destination_folder%"
)
)
@@ -83,4 +88,6 @@
)
)
-del /s /q "%source_folder%\*.pdb"
\ No newline at end of file
+del /s /q "%source_folder%\*.pdb"
+del /s /q "%destination_folder%\*.txt"
+del /s /q "%destination_folder%\*.xml"
\ No newline at end of file
diff --git a/WPFProject/Config/Database/database.db b/WPFProject/Config/Database/database.db
index 97c1f65..bca2d52 100644
--- a/WPFProject/Config/Database/database.db
+++ b/WPFProject/Config/Database/database.db
Binary files differ
diff --git a/WPFProject/PanuonWindow.xaml b/WPFProject/PanuonWindow.xaml
index b9ccb20..32a9107 100644
--- a/WPFProject/PanuonWindow.xaml
+++ b/WPFProject/PanuonWindow.xaml
@@ -27,18 +27,6 @@
<Window.Resources>
<hc:LangConverter x:Key="lc"/>
<dc:CollectionVisibility x:Key="cv"/>
- <Style x:Key="UserMenu" TargetType="Menu" BasedOn="{StaticResource {x:Static pu:StyleKeys.MenuStyle}}">
- <Setter Property="Background" Value="{StaticResource PrimaryColor}"/>
- <Setter Property="Foreground" Value="#FFF"/>
- <Setter Property="pu:IconHelper.FontFamily" Value="{StaticResource iconfont}"/>
- <Setter Property="pu:IconHelper.Margin" Value="0,0,10,0"/>
- <Setter Property="BorderThickness" Value="0"/>
- <Setter Property="Cursor" Value="Hand"/>
- <Setter Property="pu:MenuHelper.SubmenuItemsForeground" Value="#3a3a3a"/>
- <Setter Property="pu:MenuHelper.SubmenuItemsBackground" Value="White"/>
- <Setter Property="pu:MenuHelper.TopLevelItemsHoverBackground" Value="{x:Null}"/>
- <Setter Property="pu:MenuHelper.TopLevelDropDownPlacement" Value="BottomLeft"/>
- </Style>
</Window.Resources>
<Window.Background>
<ImageBrush Stretch="Uniform" AlignmentX="Left" AlignmentY="Top"
@@ -70,7 +58,30 @@
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" VerticalAlignment="Top" Margin="10,10,0,0" Orientation="Horizontal">
<Menu Margin="0" Padding="0" VerticalAlignment="Top"
- VerticalContentAlignment="Center" Style="{StaticResource UserMenu}" Background="Transparent">
+ VerticalContentAlignment="Center" Background="Transparent">
+ <Menu.Resources>
+ <ResourceDictionary>
+ <ResourceDictionary.MergedDictionaries>
+ <pu:StyleDictionary Includes="Menu" />
+ </ResourceDictionary.MergedDictionaries>
+ </ResourceDictionary>
+ </Menu.Resources>
+ <Menu.Style>
+ <Style TargetType="Menu" BasedOn="{StaticResource {x:Static pu:StyleKeys.MenuStyle}}">
+ <Setter Property="Background" Value="{StaticResource PrimaryColor}"/>
+ <Setter Property="Foreground" Value="#FFF"/>
+ <Setter Property="pu:IconHelper.FontFamily" Value="{StaticResource iconfont}"/>
+ <Setter Property="pu:IconHelper.Margin" Value="0,0,10,0"/>
+ <Setter Property="BorderThickness" Value="0"/>
+ <Setter Property="Cursor" Value="Hand"/>
+ <Setter Property="pu:MenuHelper.SubmenuItemsForeground" Value="#3a3a3a"/>
+ <Setter Property="pu:MenuHelper.SubmenuItemsBackground" Value="White"/>
+ <Setter Property="pu:MenuHelper.TopLevelItemsHoverBackground" Value="{x:Null}"/>
+ <Setter Property="pu:MenuHelper.TopLevelDropDownPlacement" Value="BottomLeft"/>
+ </Style>
+ </Menu.Style>
+
+
<MenuItem x:Name="userMenu" Header="{Binding WelcomeTitle}" FontSize="16">
<MenuItem Icon="" Cursor="Hand" FontSize="14" Header="閫�鍑虹櫥褰�"
x:Name="MiLogout" Click="MenuItem_Click"/>
@@ -134,7 +145,7 @@
HorizontalContentAlignment="Left" Style="{StaticResource LinkButton}"
Click="Clear_AllAlarm_Click"
Content="娓呴櫎鍏ㄩ儴鎶ヨ"/>
- <!--<local:ResourceUI x:Name="resourceUI" Loaded="resourceUI_Loaded" DockPanel.Dock="Right"/>-->
+ <!--<local:ResourceUI x:Name="resourceUI" Loaded="resourceUI_Loaded" DockPanel.Dock="Right"/>-->
</DockPanel>
</Grid>
diff --git a/WPFProject/PanuonWindow.xaml.cs b/WPFProject/PanuonWindow.xaml.cs
index b91ac2f..7dceeff 100644
--- a/WPFProject/PanuonWindow.xaml.cs
+++ b/WPFProject/PanuonWindow.xaml.cs
@@ -283,7 +283,16 @@
this.Minimize();
break;
case "maxBtn":
- this.Maximize();
+ //var state = this.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ //SetCurrentValue(Window.WindowStateProperty, state);
+ if (this.WindowState == WindowState.Maximized)
+ {
+ this.Normalmize();
+ }
+ else
+ {
+ this.Maximize();
+ }
break;
case "closeBtn":
this.Closed -= Logout_PanuonWindow_Closed;
diff --git a/WPFProject/WPFProject.csproj b/WPFProject/TapProject.csproj
similarity index 90%
rename from WPFProject/WPFProject.csproj
rename to WPFProject/TapProject.csproj
index 4260917..1c22b60 100644
--- a/WPFProject/WPFProject.csproj
+++ b/WPFProject/TapProject.csproj
@@ -6,7 +6,7 @@
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{D47D2314-5DD5-4899-ADF9-15ED057289D3}</ProjectGuid>
<OutputType>WinExe</OutputType>
- <RootNamespace>WPFProject</RootNamespace>
+ <RootNamespace>TapProject</RootNamespace>
<AssemblyName>TestPlatform</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
@@ -83,7 +83,7 @@
<ManifestCertificateThumbprint>EEB12C457268F9D6367680E983B284A2555FF73A</ManifestCertificateThumbprint>
</PropertyGroup>
<PropertyGroup>
- <ManifestKeyFile>WPFProject_TemporaryKey.pfx</ManifestKeyFile>
+ <ManifestKeyFile>TapProject_TemporaryKey.pfx</ManifestKeyFile>
</PropertyGroup>
<PropertyGroup>
<GenerateManifests>false</GenerateManifests>
@@ -147,6 +147,9 @@
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
+ <Reference Include="System.Runtime.CompilerServices.Unsafe, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+ <HintPath>packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
+ </Reference>
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Windows.Forms" />
@@ -434,6 +437,10 @@
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj">
+ <Project>{EFBD516D-3DC9-9D75-F082-48FBC76715F7}</Project>
+ <Name>Tap.Engine</Name>
+ </ProjectReference>
<ProjectReference Include="..\CellStepSender\CellStepSender.csproj">
<Project>{84edeac7-6763-46cd-b098-0b508aaa705f}</Project>
<Name>CellStepSender</Name>
@@ -462,6 +469,14 @@
<Project>{1260f995-1ae9-17b6-feb5-c430c1416bec}</Project>
<Name>Plugin.SeqEditor</Name>
</ProjectReference>
+ <ProjectReference Include="..\Plugin.StandardTest\Plugin.StandardTest.csproj">
+ <Project>{81de6955-406a-45d5-8c47-e02b4db23eb0}</Project>
+ <Name>Plugin.StandardTest</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Plugin.TestPlanEditor\Plugin.TestPlanEditor.csproj">
+ <Project>{ac7cc8ed-17b0-49b1-9ab8-90f72d62275f}</Project>
+ <Name>Plugin.TestPlanEditor</Name>
+ </ProjectReference>
<ProjectReference Include="..\Plugin.TestResult\Plugin.TestResult.csproj">
<Project>{7375b74c-b6bf-4814-9838-e1caacad1897}</Project>
<Name>Plugin.TestResult</Name>
@@ -469,6 +484,10 @@
<ProjectReference Include="..\Plugin.TsExecution\Plugin.TsExecution.csproj">
<Project>{202d9a7b-96b3-4ad4-9977-fc4263207a02}</Project>
<Name>Plugin.TsExecution</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Plugin.TsSupport\Plugin.TsSupport.csproj">
+ <Project>{3068124E-F12B-473E-99A0-2DA5A0385D05}</Project>
+ <Name>Plugin.TsSupport</Name>
</ProjectReference>
<ProjectReference Include="..\S7Driver\S7Driver.csproj">
<Project>{628ef12b-9ed2-4b20-950f-dc3ecc3fd15f}</Project>
@@ -542,9 +561,25 @@
<Project>{cf5bebe9-2803-49c3-b773-73d082e0984a}</Project>
<Name>HMI.Simple.Util</Name>
</ProjectReference>
+ <ProjectReference Include="..\SimpleUtil\HMI.TapApi\HMI.TapApi.csproj">
+ <Project>{eefdf6fe-41ad-4eb0-9ce6-87a07b88f9d7}</Project>
+ <Name>HMI.TapApi</Name>
+ </ProjectReference>
<ProjectReference Include="..\SimpleUtil\HMI.Util.Base\HMI.Util.Base.csproj">
<Project>{d59e25d1-9b27-46a1-8438-92661dd23fae}</Project>
<Name>HMI.Util.Base</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Aging.Infrastructure\Aging.Infrastructure.csproj">
+ <Project>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</Project>
+ <Name>Aging.Infrastructure</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Plugin.DeviceConfig\Plugin.DeviceConfig.csproj">
+ <Project>{B2C3D4E5-F6A7-8901-BCDE-F12345678901}</Project>
+ <Name>Plugin.DeviceConfig</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\Plugin.DeviceDebug\Plugin.DeviceDebug.csproj">
+ <Project>{C3D4E5F6-A7B8-9012-CDEF-123456789012}</Project>
+ <Name>Plugin.DeviceDebug</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
@@ -567,9 +602,7 @@
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
- <PostBuildEvent>IF "$(ConfigurationName)"=="Release" Confuser.CLI.exe $(TargeDir)..\ConfuserEx\$(TargetFileName).crproj
-
-
+ <PostBuildEvent>
mkdir "$(TargetDir)OpenTap"
copy "$(TargeDir)HMI.Core.dll" "$(TargetDir)OpenTap"
@@ -620,11 +653,19 @@
move /y "$(TargeDir)Util.ManualDebug.dll" $(TargeDir)Plugin\Util.ManualDebug.dll"
move /y "$(TargeDir)Plugin.QualityReport.dll" $(TargeDir)Plugin\Plugin.QualityReport.dll"
move /y "$(TargeDir)Plugin.BurnIn.dll" $(TargeDir)Plugin\Plugin.BurnIn.dll"
+move /y "$(TargeDir)Plugin.StandardTest.dll" $(TargeDir)Plugin\Plugin.StandardTest.dll"
+move /y "$(TargeDir)Plugin.TestPlanEditor.dll" $(TargeDir)Plugin\Plugin.TestPlanEditor.dll"
+move /y "$(TargeDir)Plugin.DeviceConfig.dll" $(TargeDir)Plugin\Plugin.DeviceConfig.dll"
+move /y "$(TargeDir)Plugin.DeviceDebug.dll" $(TargeDir)Plugin\Plugin.DeviceDebug.dll"
+move /y "$(TargeDir)Aging.Infrastructure.dll" $(TargeDir)Plugin\Aging.Infrastructure.dll"
move /y "$(TargeDir)AddInPlugin.dll" "$(TargetDir)OpenTap\AddInPlugin.dll"
move /y "$(TargeDir)OpenTap.dll" "$(TargetDir)OpenTap\OpenTap.dll"
move /y "$(TargeDir)OpenTap.Plugins.BasicSteps.dll" "$(TargetDir)OpenTap\OpenTap.Plugins.BasicSteps.dll"
move /y "$(TargeDir)HMI.TapApi.dll" "$(TargetDir)OpenTap\HMI.TapApi.dll"
+move /y "$(TargeDir)ViHelper.dll" "$(TargetDir)OpenTap\ViHelper.dll"
+move /y "$(TargeDir)UtilLib.dll" "$(TargetDir)OpenTap\UtilLib.dll"
+move /y "$(TargeDir)LabViewRunner.dll" "$(TargetDir)OpenTap\LabViewRunner.dll"
setlocal enabledelayedexpansion
@@ -641,7 +682,7 @@
)
for /d %25%25D in ("%25source_folder%25\*") do (
- if /I not "%25%25~nxD"=="Api" if /I not "%25%25~nxD"=="Config" if /I not "%25%25~nxD"=="Icon" if /I not "%25%25~nxD"=="Lang" if /I not "%25%25~nxD"=="Libs" if /I not "%25%25~nxD"=="Log" if /I not "%25%25~nxD"=="Plugin" if /I not "%25%25~nxD"=="TestPlatform.exe.WebView2" if /I not "%25%25~nxD"=="OpenTap" (
+ if /I not "%25%25~nxD"=="Api" if /I not "%25%25~nxD"=="Config" if /I not "%25%25~nxD"=="Icon" if /I not "%25%25~nxD"=="Lang" if /I not "%25%25~nxD"=="Libs" if /I not "%25%25~nxD"=="Log" if /I not "%25%25~nxD"=="Plugin" if /I not "%25%25~nxD"=="TestPlatform.exe.WebView2" if /I not "%25%25~nxD"=="OpenTap" if /I not "%25%25~nxD"=="Settings" if /I not "%25%25~nxD"=="Report" (
move /Y "%25%25D" "%25destination_folder%25"
)
)
@@ -652,7 +693,9 @@
)
)
-del /s /q "%25source_folder%25\*.pdb"</PostBuildEvent>
+del /s /q "%25source_folder%25\*.pdb"
+del /s /q "%25destination_folder%25\*.txt"
+del /s /q "%25destination_folder%25\*.xml"</PostBuildEvent>
</PropertyGroup>
<ProjectExtensions>
<VisualStudio>
diff --git a/WPFProject/WPFProject.sln b/WPFProject/TapProject.sln
similarity index 88%
rename from WPFProject/WPFProject.sln
rename to WPFProject/TapProject.sln
index 5546767..beba6b1 100644
--- a/WPFProject/WPFProject.sln
+++ b/WPFProject/TapProject.sln
@@ -3,7 +3,7 @@
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WPFProject", "WPFProject.csproj", "{D47D2314-5DD5-4899-ADF9-15ED057289D3}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TapProject", "TapProject.csproj", "{D47D2314-5DD5-4899-ADF9-15ED057289D3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HMI.Core", "..\HMI.Core\HMI.Core.csproj", "{3E3BF44A-0B69-45C5-9512-528CB48A7106}"
EndProject
@@ -59,8 +59,6 @@
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DIOController", "..\SimpleUtil\DIOController\DIOController.csproj", "{34FFBAFE-478F-4B6E-95E7-92A949011A9B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleTest", "..\ConsoleTest\ConsoleTest.csproj", "{2BDDB984-199B-4913-96BB-39C252E101F2}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.CurrentSensorControl", "..\Plugin.CurrentSensorControl\Plugin.CurrentSensorControl.csproj", "{E4505D51-248B-4C21-B0E3-A4B2A7551D6A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModBusTcpController", "..\ModBusTcpController\ModBusTcpController.csproj", "{49DB0C9B-B239-4608-A91E-F5ED43AFAF96}"
@@ -79,17 +77,13 @@
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.UICollection", "..\Plugin.UICollection\Plugin.UICollection.csproj", "{22DC8D91-50B2-47CA-86A8-29A6DA48F388}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilLib", "..\..\UtilLib\UtilLib.csproj", "{A132F114-A26B-4428-BAEC-4851F38C0E14}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UILib", "..\..\UILib\UILib.csproj", "{27325FE0-3CE2-4CD5-8EB5-7E45CC441479}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Util.ManualDebug", "..\Util.ManualDebug\Util.ManualDebug.csproj", "{D0944E62-EE52-4ABE-832A-A09D98814DB1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tap.Engine", "..\..\..\Project\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj", "{EFBD516D-3DC9-9D75-F082-48FBC76715F7}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tap.Engine", "..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj", "{EFBD516D-3DC9-9D75-F082-48FBC76715F7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AddInPlugin", "..\..\..\Project\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj", "{0D150CA1-D6EA-3362-7330-028939E66E1C}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AddInPlugin", "..\..\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj", "{0D150CA1-D6EA-3362-7330-028939E66E1C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tap.Plugins.BasicSteps", "..\..\..\Project\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj", "{4BCFADC0-54F9-7032-B918-E4DD407CAA76}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tap.Plugins.BasicSteps", "..\..\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj", "{4BCFADC0-54F9-7032-B918-E4DD407CAA76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.QualityReport", "..\Plugin.QualityReport\Plugin.QualityReport.csproj", "{0FD55872-B9F6-41AD-8D3F-6D7A7D3F1233}"
EndProject
@@ -98,6 +92,20 @@
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StationGlobalsReader", "..\StationGlobalsReader\StationGlobalsReader.csproj", "{87F7AD77-45E9-4185-A4F7-13041796E7B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.BurnIn", "..\Plugin.BurnIn\Plugin.BurnIn.csproj", "{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.StandardTest", "..\Plugin.StandardTest\Plugin.StandardTest.csproj", "{81DE6955-406A-45D5-8C47-E02B4DB23EB0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UILib", "..\..\..\VisualStudio\UILib\UILib.csproj", "{27325FE0-3CE2-4CD5-8EB5-7E45CC441479}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilLib", "..\..\..\VisualStudio\UtilLib\UtilLib.csproj", "{A132F114-A26B-4428-BAEC-4851F38C0E14}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.TestPlanEditor", "..\Plugin.TestPlanEditor\Plugin.TestPlanEditor.csproj", "{AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeqEditor", "..\..\OpenTapEditor\Editor\SeqEditor.csproj", "{BCE23F46-610B-0033-562D-FF3FE4107381}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.DeviceDebug", "..\Plugin.DeviceDebug\Plugin.DeviceDebug.csproj", "{8B575277-3123-FC91-8B2F-B274E830409F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plugin.DeviceConfig", "..\Plugin.DeviceConfig\Plugin.DeviceConfig.csproj", "{0FEB8A27-7309-11F3-45F0-91187046F7F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -141,8 +149,8 @@
{F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Debug|x86.Build.0 = Debug|Any CPU
{F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|Any CPU.Build.0 = Release|Any CPU
- {F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x64.ActiveCfg = Release|Any CPU
- {F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x64.Build.0 = Release|Any CPU
+ {F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x64.ActiveCfg = Release|x64
+ {F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x64.Build.0 = Release|x64
{F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x86.ActiveCfg = Release|Any CPU
{F17DE6B5-AF1D-48C6-94A0-6B763A6546A6}.Release|x86.Build.0 = Release|Any CPU
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -153,8 +161,8 @@
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Debug|x86.Build.0 = Debug|Any CPU
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|Any CPU.Build.0 = Release|Any CPU
- {DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x64.ActiveCfg = Release|Any CPU
- {DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x64.Build.0 = Release|Any CPU
+ {DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x64.ActiveCfg = Release|x64
+ {DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x64.Build.0 = Release|x64
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x86.ActiveCfg = Release|Any CPU
{DE4205C6-5923-46FD-A4B9-65169BF61C75}.Release|x86.Build.0 = Release|Any CPU
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -165,8 +173,8 @@
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Debug|x86.Build.0 = Debug|Any CPU
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|Any CPU.ActiveCfg = Release|Any CPU
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|Any CPU.Build.0 = Release|Any CPU
- {325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x64.ActiveCfg = Release|Any CPU
- {325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x64.Build.0 = Release|Any CPU
+ {325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x64.ActiveCfg = Release|x64
+ {325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x64.Build.0 = Release|x64
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x86.ActiveCfg = Release|Any CPU
{325A7F52-FE51-44F0-B3AC-5F42F5356605}.Release|x86.Build.0 = Release|Any CPU
{945C2577-4558-45E3-891A-096CB3047304}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -177,8 +185,8 @@
{945C2577-4558-45E3-891A-096CB3047304}.Debug|x86.Build.0 = Debug|Any CPU
{945C2577-4558-45E3-891A-096CB3047304}.Release|Any CPU.ActiveCfg = Release|Any CPU
{945C2577-4558-45E3-891A-096CB3047304}.Release|Any CPU.Build.0 = Release|Any CPU
- {945C2577-4558-45E3-891A-096CB3047304}.Release|x64.ActiveCfg = Release|Any CPU
- {945C2577-4558-45E3-891A-096CB3047304}.Release|x64.Build.0 = Release|Any CPU
+ {945C2577-4558-45E3-891A-096CB3047304}.Release|x64.ActiveCfg = Release|x64
+ {945C2577-4558-45E3-891A-096CB3047304}.Release|x64.Build.0 = Release|x64
{945C2577-4558-45E3-891A-096CB3047304}.Release|x86.ActiveCfg = Release|Any CPU
{945C2577-4558-45E3-891A-096CB3047304}.Release|x86.Build.0 = Release|Any CPU
{3DD748AC-9708-49AD-B846-659214035588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -189,8 +197,8 @@
{3DD748AC-9708-49AD-B846-659214035588}.Debug|x86.Build.0 = Debug|Any CPU
{3DD748AC-9708-49AD-B846-659214035588}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3DD748AC-9708-49AD-B846-659214035588}.Release|Any CPU.Build.0 = Release|Any CPU
- {3DD748AC-9708-49AD-B846-659214035588}.Release|x64.ActiveCfg = Release|Any CPU
- {3DD748AC-9708-49AD-B846-659214035588}.Release|x64.Build.0 = Release|Any CPU
+ {3DD748AC-9708-49AD-B846-659214035588}.Release|x64.ActiveCfg = Release|x64
+ {3DD748AC-9708-49AD-B846-659214035588}.Release|x64.Build.0 = Release|x64
{3DD748AC-9708-49AD-B846-659214035588}.Release|x86.ActiveCfg = Release|Any CPU
{3DD748AC-9708-49AD-B846-659214035588}.Release|x86.Build.0 = Release|Any CPU
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -201,8 +209,8 @@
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Debug|x86.Build.0 = Debug|Any CPU
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|Any CPU.Build.0 = Release|Any CPU
- {2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x64.ActiveCfg = Release|Any CPU
- {2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x64.Build.0 = Release|Any CPU
+ {2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x64.ActiveCfg = Release|x64
+ {2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x64.Build.0 = Release|x64
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x86.ActiveCfg = Release|Any CPU
{2A6F9720-A890-4036-9D73-41A0DAE28341}.Release|x86.Build.0 = Release|Any CPU
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -213,8 +221,8 @@
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Debug|x86.Build.0 = Debug|Any CPU
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|Any CPU.Build.0 = Release|Any CPU
- {A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x64.ActiveCfg = Release|Any CPU
- {A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x64.Build.0 = Release|Any CPU
+ {A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x64.ActiveCfg = Release|x64
+ {A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x64.Build.0 = Release|x64
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x86.ActiveCfg = Release|Any CPU
{A580DE4A-34E7-4EFE-AD2C-0871396000E9}.Release|x86.Build.0 = Release|Any CPU
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -225,8 +233,8 @@
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Debug|x86.Build.0 = Debug|Any CPU
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|Any CPU.Build.0 = Release|Any CPU
- {736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x64.ActiveCfg = Release|Any CPU
- {736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x64.Build.0 = Release|Any CPU
+ {736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x64.ActiveCfg = Release|x64
+ {736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x64.Build.0 = Release|x64
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x86.ActiveCfg = Release|Any CPU
{736CD77D-B81E-4866-BB00-84B0B57110F0}.Release|x86.Build.0 = Release|Any CPU
{20C23602-B029-4E21-8C11-B9E5F2223237}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -237,8 +245,8 @@
{20C23602-B029-4E21-8C11-B9E5F2223237}.Debug|x86.Build.0 = Debug|Any CPU
{20C23602-B029-4E21-8C11-B9E5F2223237}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20C23602-B029-4E21-8C11-B9E5F2223237}.Release|Any CPU.Build.0 = Release|Any CPU
- {20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x64.ActiveCfg = Release|Any CPU
- {20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x64.Build.0 = Release|Any CPU
+ {20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x64.ActiveCfg = Release|x64
+ {20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x64.Build.0 = Release|x64
{20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x86.ActiveCfg = Release|Any CPU
{20C23602-B029-4E21-8C11-B9E5F2223237}.Release|x86.Build.0 = Release|Any CPU
{7375B74C-B6BF-4814-9838-E1CAACAD1897}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -273,8 +281,8 @@
{AFEA100A-9223-448D-AB10-3015D93DD98C}.Debug|x86.Build.0 = Debug|Any CPU
{AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|Any CPU.Build.0 = Release|Any CPU
- {AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x64.ActiveCfg = Release|Any CPU
- {AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x64.Build.0 = Release|Any CPU
+ {AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x64.ActiveCfg = Release|x64
+ {AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x64.Build.0 = Release|x64
{AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x86.ActiveCfg = Release|Any CPU
{AFEA100A-9223-448D-AB10-3015D93DD98C}.Release|x86.Build.0 = Release|Any CPU
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -285,8 +293,8 @@
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Debug|x86.Build.0 = Debug|Any CPU
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|Any CPU.Build.0 = Release|Any CPU
- {306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x64.ActiveCfg = Release|Any CPU
- {306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x64.Build.0 = Release|Any CPU
+ {306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x64.ActiveCfg = Release|x64
+ {306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x64.Build.0 = Release|x64
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x86.ActiveCfg = Release|Any CPU
{306A83DC-A7DB-4057-BAA7-477D8756F5D9}.Release|x86.Build.0 = Release|Any CPU
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -297,8 +305,8 @@
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Debug|x86.Build.0 = Debug|Any CPU
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|Any CPU.Build.0 = Release|Any CPU
- {D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x64.ActiveCfg = Release|Any CPU
- {D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x64.Build.0 = Release|Any CPU
+ {D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x64.ActiveCfg = Release|x64
+ {D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x64.Build.0 = Release|x64
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x86.ActiveCfg = Release|Any CPU
{D0806FA6-3F6D-4304-81B8-EAD9DA7CB5B2}.Release|x86.Build.0 = Release|Any CPU
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -309,8 +317,8 @@
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Debug|x86.Build.0 = Debug|Any CPU
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|Any CPU.Build.0 = Release|Any CPU
- {E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x64.ActiveCfg = Release|Any CPU
- {E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x64.Build.0 = Release|Any CPU
+ {E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x64.ActiveCfg = Release|x64
+ {E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x64.Build.0 = Release|x64
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x86.ActiveCfg = Release|Any CPU
{E5740BB5-9FC6-4A20-B7C2-CC90C281ED05}.Release|x86.Build.0 = Release|Any CPU
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -321,8 +329,8 @@
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Debug|x86.Build.0 = Debug|Any CPU
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|Any CPU.Build.0 = Release|Any CPU
- {BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x64.ActiveCfg = Release|Any CPU
- {BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x64.Build.0 = Release|Any CPU
+ {BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x64.ActiveCfg = Release|x64
+ {BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x64.Build.0 = Release|x64
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x86.ActiveCfg = Release|Any CPU
{BE42AB26-3270-43E1-84B8-6929D862FF8D}.Release|x86.Build.0 = Release|Any CPU
{CF5BEBE9-2803-49C3-B773-73D082E0984A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -345,8 +353,8 @@
{6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Debug|x86.Build.0 = Debug|Any CPU
{6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|Any CPU.Build.0 = Release|Any CPU
- {6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x64.ActiveCfg = Release|Any CPU
- {6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x64.Build.0 = Release|Any CPU
+ {6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x64.ActiveCfg = Release|x64
+ {6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x64.Build.0 = Release|x64
{6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x86.ActiveCfg = Release|Any CPU
{6735FDB8-B471-41C3-93CB-E8ADBD0989F0}.Release|x86.Build.0 = Release|Any CPU
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -357,8 +365,8 @@
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Debug|x86.Build.0 = Debug|Any CPU
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|Any CPU.Build.0 = Release|Any CPU
- {6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x64.ActiveCfg = Release|Any CPU
- {6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x64.Build.0 = Release|Any CPU
+ {6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x64.ActiveCfg = Release|x64
+ {6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x64.Build.0 = Release|x64
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x86.ActiveCfg = Release|Any CPU
{6D0AAB08-0F3E-48CE-B755-9D29BC8F1225}.Release|x86.Build.0 = Release|Any CPU
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -369,8 +377,8 @@
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Debug|x86.Build.0 = Debug|Any CPU
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|Any CPU.Build.0 = Release|Any CPU
- {D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x64.ActiveCfg = Release|Any CPU
- {D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x64.Build.0 = Release|Any CPU
+ {D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x64.ActiveCfg = Release|x64
+ {D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x64.Build.0 = Release|x64
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x86.ActiveCfg = Release|Any CPU
{D59E25D1-9B27-46A1-8438-92661DD23FAE}.Release|x86.Build.0 = Release|Any CPU
{F8B8B6B6-821B-4FF6-B556-9CAB840EA57A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -393,8 +401,8 @@
{61DF853D-829A-4426-BEF8-0135586005B6}.Debug|x86.Build.0 = Debug|Any CPU
{61DF853D-829A-4426-BEF8-0135586005B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61DF853D-829A-4426-BEF8-0135586005B6}.Release|Any CPU.Build.0 = Release|Any CPU
- {61DF853D-829A-4426-BEF8-0135586005B6}.Release|x64.ActiveCfg = Release|Any CPU
- {61DF853D-829A-4426-BEF8-0135586005B6}.Release|x64.Build.0 = Release|Any CPU
+ {61DF853D-829A-4426-BEF8-0135586005B6}.Release|x64.ActiveCfg = Release|x64
+ {61DF853D-829A-4426-BEF8-0135586005B6}.Release|x64.Build.0 = Release|x64
{61DF853D-829A-4426-BEF8-0135586005B6}.Release|x86.ActiveCfg = Release|Any CPU
{61DF853D-829A-4426-BEF8-0135586005B6}.Release|x86.Build.0 = Release|Any CPU
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -405,8 +413,8 @@
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Debug|x86.Build.0 = Debug|Any CPU
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|Any CPU.Build.0 = Release|Any CPU
- {3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x64.ActiveCfg = Release|Any CPU
- {3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x64.Build.0 = Release|Any CPU
+ {3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x64.ActiveCfg = Release|x64
+ {3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x64.Build.0 = Release|x64
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x86.ActiveCfg = Release|x86
{3068124E-F12B-473E-99A0-2DA5A0385D05}.Release|x86.Build.0 = Release|x86
{76CE4D21-702C-416F-95B4-F4F4C5B2D162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -433,18 +441,6 @@
{34FFBAFE-478F-4B6E-95E7-92A949011A9B}.Release|x64.Build.0 = Release|Any CPU
{34FFBAFE-478F-4B6E-95E7-92A949011A9B}.Release|x86.ActiveCfg = Release|Any CPU
{34FFBAFE-478F-4B6E-95E7-92A949011A9B}.Release|x86.Build.0 = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|x64.ActiveCfg = Debug|x64
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|x64.Build.0 = Debug|x64
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Debug|x86.Build.0 = Debug|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|Any CPU.Build.0 = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|x64.ActiveCfg = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|x64.Build.0 = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|x86.ActiveCfg = Release|Any CPU
- {2BDDB984-199B-4913-96BB-39C252E101F2}.Release|x86.Build.0 = Release|Any CPU
{E4505D51-248B-4C21-B0E3-A4B2A7551D6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4505D51-248B-4C21-B0E3-A4B2A7551D6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4505D51-248B-4C21-B0E3-A4B2A7551D6A}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -553,30 +549,6 @@
{22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x64.Build.0 = Release|Any CPU
{22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x86.ActiveCfg = Release|Any CPU
{22DC8D91-50B2-47CA-86A8-29A6DA48F388}.Release|x86.Build.0 = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.Build.0 = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.Build.0 = Debug|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.Build.0 = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.ActiveCfg = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.Build.0 = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.ActiveCfg = Release|Any CPU
- {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.Build.0 = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.ActiveCfg = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.Build.0 = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.ActiveCfg = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.Build.0 = Debug|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.Build.0 = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.ActiveCfg = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.Build.0 = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.ActiveCfg = Release|Any CPU
- {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.Build.0 = Release|Any CPU
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x64.ActiveCfg = Debug|x64
@@ -585,8 +557,8 @@
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Debug|x86.Build.0 = Debug|x86
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|Any CPU.Build.0 = Release|Any CPU
- {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.ActiveCfg = Release|Any CPU
- {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.Build.0 = Release|Any CPU
+ {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.ActiveCfg = Release|x64
+ {D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x64.Build.0 = Release|x64
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x86.ActiveCfg = Release|x86
{D0944E62-EE52-4ABE-832A-A09D98814DB1}.Release|x86.Build.0 = Release|x86
{EFBD516D-3DC9-9D75-F082-48FBC76715F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -669,10 +641,94 @@
{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Debug|x86.Build.0 = Debug|Any CPU
{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|Any CPU.Build.0 = Release|Any CPU
- {7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x64.ActiveCfg = Release|Any CPU
- {7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x64.Build.0 = Release|Any CPU
+ {7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x64.ActiveCfg = Release|x64
+ {7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x64.Build.0 = Release|x64
{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x86.ActiveCfg = Release|Any CPU
{7964F444-D2C6-4C11-87F4-0DF4FAEC10B6}.Release|x86.Build.0 = Release|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|x64.ActiveCfg = Debug|x64
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|x64.Build.0 = Debug|x64
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Debug|x86.Build.0 = Debug|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|x64.ActiveCfg = Release|x64
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|x64.Build.0 = Release|x64
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|x86.ActiveCfg = Release|Any CPU
+ {81DE6955-406A-45D5-8C47-E02B4DB23EB0}.Release|x86.Build.0 = Release|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.ActiveCfg = Debug|x64
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x64.Build.0 = Debug|x64
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Debug|x86.Build.0 = Debug|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.ActiveCfg = Release|x64
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x64.Build.0 = Release|x64
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.ActiveCfg = Release|Any CPU
+ {27325FE0-3CE2-4CD5-8EB5-7E45CC441479}.Release|x86.Build.0 = Release|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.ActiveCfg = Debug|x64
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x64.Build.0 = Debug|x64
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Debug|x86.Build.0 = Debug|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.ActiveCfg = Release|x64
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x64.Build.0 = Release|x64
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.ActiveCfg = Release|Any CPU
+ {A132F114-A26B-4428-BAEC-4851F38C0E14}.Release|x86.Build.0 = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|x64.Build.0 = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Debug|x86.Build.0 = Debug|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|x64.ActiveCfg = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|x64.Build.0 = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|x86.ActiveCfg = Release|Any CPU
+ {AC7CC8ED-17B0-49B1-9AB8-90F72D62275F}.Release|x86.Build.0 = Release|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|x64.ActiveCfg = Debug|x64
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|x64.Build.0 = Debug|x64
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Debug|x86.Build.0 = Debug|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|x64.ActiveCfg = Release|x64
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|x64.Build.0 = Release|x64
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|x86.ActiveCfg = Release|Any CPU
+ {BCE23F46-610B-0033-562D-FF3FE4107381}.Release|x86.Build.0 = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|x64.Build.0 = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Debug|x86.Build.0 = Debug|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|x64.ActiveCfg = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|x64.Build.0 = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|x86.ActiveCfg = Release|Any CPU
+ {8B575277-3123-FC91-8B2F-B274E830409F}.Release|x86.Build.0 = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|x64.Build.0 = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Debug|x86.Build.0 = Debug|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|x64.ActiveCfg = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|x64.Build.0 = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|x86.ActiveCfg = Release|Any CPU
+ {0FEB8A27-7309-11F3-45F0-91187046F7F4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/WPFProject/packages.config b/WPFProject/packages.config
index b62d154..f6fe8f8 100644
--- a/WPFProject/packages.config
+++ b/WPFProject/packages.config
@@ -5,4 +5,5 @@
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net472" />
<package id="Panuon.WPF" version="1.1.3" targetFramework="net472" />
<package id="Panuon.WPF.UI" version="1.3.0.2" targetFramework="net472" />
+ <package id="System.Runtime.CompilerServices.Unsafe" version="6.0.0" targetFramework="net472" />
</packages>
\ No newline at end of file
diff --git a/check_parent.py b/check_parent.py
new file mode 100644
index 0000000..18acb88
--- /dev/null
+++ b/check_parent.py
@@ -0,0 +1,38 @@
+import sqlite3
+
+db_path = r'd:\TestPlatform\TapProject\WPFProject\bin\x64\Debug\Config\Database\database.db'
+conn = sqlite3.connect(db_path)
+c = conn.cursor()
+
+# Check if ManualControl parent menu has a role_menu_rel entry
+print("=== ManualControl menu entry ===")
+c.execute("SELECT * FROM menu WHERE code='ManualControl'")
+for r in c.fetchall():
+ print(f" {r}")
+
+print("\n=== role_menu_rel for ManualControl ===")
+c.execute("SELECT * FROM role_menu_rel WHERE menuCode='ManualControl'")
+for r in c.fetchall():
+ print(f" {r}")
+
+# Check all parent menus and their role relations
+print("\n=== All parent menus (type='' or empty dllPath) ===")
+c.execute("SELECT m.code, m.name, m.type, m.parentCode, rrel.roleCode FROM menu m LEFT JOIN role_menu_rel rrel ON m.code = rrel.menuCode WHERE m.type = '' OR m.type IS NULL")
+for r in c.fetchall():
+ print(f" {r}")
+
+# Simulate the full login query for admin user
+print("\n=== Full menu tree for admin (all levels) ===")
+c.execute("""
+ SELECT DISTINCT m.code, m.name, m.type, m.parentCode, m.level, m.dllPath
+ FROM menu AS m
+ INNER JOIN role_menu_rel AS rrel ON m.code = rrel.menuCode
+ INNER JOIN role AS r ON rrel.roleCode = r.code
+ INNER JOIN user_role_rel AS urel ON r.code = urel.roleCode
+ AND urel.username = 'admin'
+ ORDER BY m.level, m.id
+""")
+for r in c.fetchall():
+ print(f" {r}")
+
+conn.close()
diff --git a/debug_check.py b/debug_check.py
new file mode 100644
index 0000000..a4fa793
--- /dev/null
+++ b/debug_check.py
@@ -0,0 +1,34 @@
+import sqlite3
+
+# Check both databases
+for label, db_path in [
+ ("Output DB", r'd:\TestPlatform\TapProject\WPFProject\bin\x64\Debug\Config\Database\database.db'),
+ ("Source DB", r'd:\TestPlatform\TapProject\WPFProject\Config\Database\database.db')
+]:
+ print(f"\n=== {label} ===")
+ conn = sqlite3.connect(db_path)
+ c = conn.cursor()
+
+ # Check schema
+ c.execute("PRAGMA table_info(menu)")
+ cols = [row[1] for row in c.fetchall()]
+ print(f"Columns: {cols}")
+
+ # Check our entries
+ c.execute("SELECT * FROM menu WHERE code IN ('DeviceConfig','DeviceDebug')")
+ for r in c.fetchall():
+ print(f" Menu: {r}")
+
+ # Check comparison with working entries
+ c.execute("SELECT code, name, level, type, parentCode FROM menu WHERE parentCode='ManualControl'")
+ print("\n ManualControl children:")
+ for r in c.fetchall():
+ print(f" {r}")
+
+ # Check role_menu_rel
+ c.execute("SELECT * FROM role_menu_rel WHERE menuCode IN ('DeviceConfig','DeviceDebug')")
+ print("\n role_menu_rel:")
+ for r in c.fetchall():
+ print(f" {r}")
+
+ conn.close()
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204.drawio" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204.drawio"
new file mode 100644
index 0000000..f9304d6
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204.drawio"
@@ -0,0 +1,93 @@
+<mxfile host="Electron" modified="2026-09-10T00:00:00.000Z" agent="Codex" version="24.7.17" type="device">
+ <diagram id="tap-overview" name="01-鎬讳綋鍒嗗眰缁撴瀯">
+ <mxGraphModel dx="1680" dy="1188" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1680" pageHeight="1188" math="0" shadow="0" background="#FFFFFF">
+ <root>
+ <mxCell id="0"/>
+ <mxCell id="1" parent="0"/>
+ <mxCell id="p1_title" value="TapProject 浠g爜鎬讳綋鍒嗗眰缁撴瀯" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Microsoft YaHei;fontSize=18;fontStyle=1;fontColor=#17365D;" vertex="1" parent="1"><mxGeometry x="60" y="20" width="1180" height="36" as="geometry"/></mxCell>
+ <mxCell id="p1_subtitle" value="闈欐�佹簮鐮佸揩鐓э細45 涓唴閮ㄥ伐绋嬶綔.NET Framework 4.7.2锝滃疄绾匡紳杩愯/鎵胯浇閾捐矾锛岀伆鑹茶櫄绾匡紳缂栬瘧鎴栨鏋朵緷璧�" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#666666;" vertex="1" parent="1"><mxGeometry x="60" y="56" width="1500" height="24" as="geometry"/></mxCell>
+ <mxCell id="p1_host_layer" value="WPF 瀹夸富涓庡惎鍔ㄥ眰" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#F4F8FC;strokeColor=#7EA6D8;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#17365D;" vertex="1" parent="1"><mxGeometry x="60" y="95" width="1560" height="185" as="geometry"/></mxCell>
+ <mxCell id="p1_app" value="<b>App / TestPlatform</b><br>鍗曞疄渚嬨�佹棩蹇椼�佽瑷�銆佽祫婧�<br>绋嬪簭闆嗚В鏋愪笌鍚姩" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#17365D;spacing=8;" vertex="1" parent="1"><mxGeometry x="95" y="145" width="285" height="95" as="geometry"/></mxCell>
+ <mxCell id="p1_login" value="<b>Login + UserInfo</b><br>SQLite 鐢ㄦ埛/瑙掕壊/鑿滃崟<br>鐢熸垚鏉冮檺涓婁笅鏂�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#17365D;spacing=8;" vertex="1" parent="1"><mxGeometry x="425" y="145" width="285" height="95" as="geometry"/></mxCell>
+ <mxCell id="p1_loader" value="<b>LoadingWindow + PluginLoader</b><br>鏉冮檺鑿滃崟鏍戙�佸弽灏勮杞� DLL<br>娉ㄥ叆鏃ュ織/閰嶇疆骞跺垵濮嬪寲" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#17365D;spacing=8;" vertex="1" parent="1"><mxGeometry x="755" y="145" width="320" height="95" as="geometry"/></mxCell>
+ <mxCell id="p1_host" value="<b>PanuonWindow + MainTabControl</b><br>IPluginContainer銆佽彍鍗曚笌椤电鎵胯浇<br>Active / DeActive / Close 鐢熷懡鍛ㄦ湡" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#17365D;spacing=8;" vertex="1" parent="1"><mxGeometry x="1120" y="145" width="450" height="95" as="geometry"/></mxCell>
+ <mxCell id="p1_plugin_layer" value="涓氬姟涓庡姛鑳芥彃浠跺眰" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9F0;strokeColor=#E6A23C;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#7F6000;" vertex="1" parent="1"><mxGeometry x="60" y="315" width="1560" height="300" as="geometry"/></mxCell>
+ <mxCell id="p1_burnin" value="<b>鑰佸寲涓讳笟鍔★紙鏍稿績锛�</b><br><br>Plugin.BurnIn<br>鐐変綋/绌翠綅銆佹壂鐮併�丳LC銆丮ES<br>娴嬭瘯鐘舵�併�佹洸绾裤�丼QLite/CSV<br><br>Plugin.CurrentSensorControl<br>Plugin.DOControl / DODriverControl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FCE4D6;strokeColor=#ED7D31;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#843C0C;spacing=10;" vertex="1" parent="1"><mxGeometry x="95" y="365" width="340" height="205" as="geometry"/></mxCell>
+ <mxCell id="p1_test_plugins" value="<b>娴嬭瘯鎵ц涓庣紪杈�</b><br><br>Plugin.TsExecution<br>Plugin.StandardTest<br>Plugin.TestPlanEditor<br>Plugin.SeqEditor<br>Util.ManualDebug" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#7F6000;spacing=10;" vertex="1" parent="1"><mxGeometry x="470" y="365" width="340" height="205" as="geometry"/></mxCell>
+ <mxCell id="p1_data_plugins" value="<b>鏁版嵁銆佺粨鏋滀笌鎶ュ憡</b><br><br>Plugin.TestResult<br>Plugin.DataViewer / DataBoard<br>Plugin.QualityReport<br>HMI.DataViewer<br>HMI.LogViewer" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E2F0D9;strokeColor=#70AD47;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#375623;spacing=10;" vertex="1" parent="1"><mxGeometry x="845" y="365" width="340" height="205" as="geometry"/></mxCell>
+ <mxCell id="p1_manage_plugins" value="<b>绯荤粺绠$悊涓庡叾浠栨彃浠�</b><br><br>Menu / Setting / Parameter<br>AlarmInfo / ModelManage<br>JarUI / UICollection<br><br>鑿滃崟涓庣敤鎴锋潈闄愬喅瀹氬疄闄呰杞介泦鍚�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E4DFEC;strokeColor=#8064A2;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#4C3768;spacing=10;" vertex="1" parent="1"><mxGeometry x="1220" y="365" width="350" height="205" as="geometry"/></mxCell>
+ <mxCell id="p1_framework_layer" value="骞冲彴妗嗘灦涓庡叕鍏辫兘鍔涘眰" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#F5F5F5;strokeColor=#7F8C8D;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#37474F;" vertex="1" parent="1"><mxGeometry x="60" y="650" width="1560" height="200" as="geometry"/></mxCell>
+ <mxCell id="p1_core" value="<b>HMI.Core</b><br>IPlugin 濂戠害銆佸疄浣撱�佽缃�<br>Manager銆丼QLite銆佽矾寰�/璇█" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#37474F;spacing=8;" vertex="1" parent="1"><mxGeometry x="95" y="700" width="280" height="105" as="geometry"/></mxCell>
+ <mxCell id="p1_base" value="<b>HMI.Base</b><br>PluginLoader<br>PluginControl / MaskControl" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#37474F;spacing=8;" vertex="1" parent="1"><mxGeometry x="405" y="700" width="260" height="105" as="geometry"/></mxCell>
+ <mxCell id="p1_ui" value="<b>HMI.UI / StdControl</b><br>鍏叡鎺т欢銆佹牱寮忋�佽浆鎹㈠櫒<br>WPF/WinForms 鎵胯浇" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#37474F;spacing=8;" vertex="1" parent="1"><mxGeometry x="695" y="700" width="280" height="105" as="geometry"/></mxCell>
+ <mxCell id="p1_util" value="<b>HMI.Util + SimpleUtil</b><br>閰嶇疆銆佹棩蹇椼�佷覆鍙c�丮SMQ<br>搴忓垪鍖栦笌閫氱敤鏁版嵁璁块棶" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#37474F;spacing=8;" vertex="1" parent="1"><mxGeometry x="1005" y="700" width="280" height="105" as="geometry"/></mxCell>
+ <mxCell id="p1_ts_support" value="<b>TsSupport / TapApi</b><br>ITestExecutionWrapper<br>TestStand / OpenTAP 閫傞厤" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#37474F;spacing=8;" vertex="1" parent="1"><mxGeometry x="1315" y="700" width="255" height="105" as="geometry"/></mxCell>
+ <mxCell id="p1_infra_layer" value="璁惧銆侀�氫俊銆佹暟鎹笌澶栭儴绯荤粺" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#F2F8F8;strokeColor=#4F9DA6;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#1F4E5F;" vertex="1" parent="1"><mxGeometry x="60" y="890" width="1560" height="250" as="geometry"/></mxCell>
+ <mxCell id="p1_plc_adapter" value="<b>PLC 閫傞厤</b><br>PLCHelper / S7Driver" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="100" y="945" width="300" height="65" as="geometry"/></mxCell>
+ <mxCell id="p1_plc_ext" value="瑗块棬瀛� S7 PLC / 鑰佸寲鐐夋帶鍒跺櫒" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#5B9BD5;dashed=1;fontFamily=Microsoft YaHei;fontSize=11;" vertex="1" parent="1"><mxGeometry x="100" y="1045" width="300" height="55" as="geometry"/></mxCell>
+ <mxCell id="p1_comm_adapter" value="<b>閫氫俊涓� I/O</b><br>OPC / Modbus TCP / DIO / TCP / Serial" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="480" y="945" width="300" height="65" as="geometry"/></mxCell>
+ <mxCell id="p1_comm_ext" value="鎵爜鍣ㄣ�佷紶鎰熷櫒銆佹澘鍗′笌缃戠粶璁惧" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#5B9BD5;dashed=1;fontFamily=Microsoft YaHei;fontSize=11;" vertex="1" parent="1"><mxGeometry x="480" y="1045" width="300" height="55" as="geometry"/></mxCell>
+ <mxCell id="p1_exec_adapter" value="<b>娴嬭瘯鎵ц閫傞厤</b><br>TapWrapper / TeststandWrapper" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="860" y="945" width="300" height="65" as="geometry"/></mxCell>
+ <mxCell id="p1_exec_ext" value="NI TestStand / OpenTAP锛堝閮ㄦ簮宸ョ▼褰撳墠缂哄け锛�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#C9A227;dashed=1;fontFamily=Microsoft YaHei;fontSize=11;" vertex="1" parent="1"><mxGeometry x="860" y="1045" width="300" height="55" as="geometry"/></mxCell>
+ <mxCell id="p1_data_adapter" value="<b>鏁版嵁涓庝紒涓氶泦鎴�</b><br>SQLite / CSV / Config / Log / HTTP" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="1240" y="945" width="300" height="65" as="geometry"/></mxCell>
+ <mxCell id="p1_data_ext" value="鏈湴鏂囦欢涓庢暟鎹簱 / MES 鏈嶅姟" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFFFFF;strokeColor=#5B9BD5;dashed=1;fontFamily=Microsoft YaHei;fontSize=11;" vertex="1" parent="1"><mxGeometry x="1240" y="1045" width="300" height="55" as="geometry"/></mxCell>
+ <mxCell id="p1_e1" value="鍚姩" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p1_app" target="p1_login"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e2" value="鐧诲綍鎴愬姛" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p1_login" target="p1_loader"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e3" value="瑁呰浇瀹屾垚" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p1_loader" target="p1_host"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e4" value="鎵胯浇涓庡垏鎹�" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p1_host" target="p1_plugin_layer"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e5" value="鎻掍欢濂戠害涓庡叕鍏辫兘鍔�" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;strokeColor=#808080;strokeWidth=1.5;dashed=1;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p1_plugin_layer" target="p1_framework_layer"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e6" value="閫傞厤涓庢寔涔呭寲" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=open;endFill=0;strokeColor=#808080;strokeWidth=1.5;dashed=1;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p1_framework_layer" target="p1_infra_layer"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e7" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#4F9DA6;strokeWidth=1.5;" edge="1" parent="1" source="p1_plc_adapter" target="p1_plc_ext"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e8" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#4F9DA6;strokeWidth=1.5;" edge="1" parent="1" source="p1_comm_adapter" target="p1_comm_ext"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e9" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#4F9DA6;strokeWidth=1.5;" edge="1" parent="1" source="p1_exec_adapter" target="p1_exec_ext"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p1_e10" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#4F9DA6;strokeWidth=1.5;" edge="1" parent="1" source="p1_data_adapter" target="p1_data_ext"><mxGeometry relative="1" as="geometry"/></mxCell>
+ </root>
+ </mxGraphModel>
+ </diagram>
+ <diagram id="tap-runtime" name="02-杩愯鏃朵笌鑰佸寲涓婚摼璺�">
+ <mxGraphModel dx="1680" dy="1188" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1680" pageHeight="1188" math="0" shadow="0" background="#FFFFFF">
+ <root>
+ <mxCell id="0"/>
+ <mxCell id="1" parent="0"/>
+ <mxCell id="p2_title" value="TapProject 杩愯鏃惰杞戒笌鑰佸寲娴嬭瘯涓婚摼璺�" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Microsoft YaHei;fontSize=18;fontStyle=1;fontColor=#17365D;" vertex="1" parent="1"><mxGeometry x="60" y="20" width="1300" height="36" as="geometry"/></mxCell>
+ <mxCell id="p2_subtitle" value="鍏崇郴鏉ヨ嚜 App/Login/PluginLoader/MainTabControl 涓� Plugin.BurnIn銆丳lugin.TsSupport 鐨勯潤鎬佷唬鐮侊紱閮ㄧ讲閰嶇疆鍜岃澶囧弬鏁版湭鑱旀満楠岃瘉" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#666666;" vertex="1" parent="1"><mxGeometry x="60" y="56" width="1500" height="24" as="geometry"/></mxCell>
+ <mxCell id="p2_start_layer" value="A. 鍚姩銆佽璇佷笌鎻掍欢瑁呰浇锛堜粠宸﹀埌鍙筹級" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#F4F8FC;strokeColor=#7EA6D8;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#17365D;" vertex="1" parent="1"><mxGeometry x="60" y="100" width="1560" height="335" as="geometry"/></mxCell>
+ <mxCell id="p2_app" value="<b>App</b><br>鍗曞疄渚嬩繚鎶�<br>鏃ュ織/璇█/璧勬簮鍒濆鍖�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="90" y="175" width="215" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_login" value="<b>Login</b><br>鏍¢獙鐢ㄦ埛<br>鏌ヨ瑙掕壊涓庤彍鍗曟潈闄�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="345" y="175" width="215" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_user" value="<b>UserInfo</b><br>Permissions<br>AllPermissions" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="600" y="175" width="200" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_loading" value="<b>LoadingWindow</b><br>AlarmUtil.Init<br>PluginLoader.LoadMenuAndPlugin" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="840" y="175" width="250" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_reflect" value="<b>鍙嶅皠瑁呰浇</b><br>Assembly.LoadFrom<br>Activator.CreateInstance" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="1130" y="175" width="220" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_host" value="<b>PanuonWindow</b><br>MainTabControl<br>Active / DeActive / Close" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#D9EAF7;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="1390" y="175" width="195" height="105" as="geometry"/></mxCell>
+ <mxCell id="p2_sqlite" value="<b>SQLite database.db</b><br>user / role / menu / permission 鍏崇郴" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E2F0D9;strokeColor=#70AD47;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="345" y="330" width="455" height="70" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e1" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;" edge="1" parent="1" source="p2_app" target="p2_login"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e2" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;" edge="1" parent="1" source="p2_login" target="p2_user"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e3" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;" edge="1" parent="1" source="p2_user" target="p2_loading"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e4" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;" edge="1" parent="1" source="p2_loading" target="p2_reflect"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e5" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#1F4E79;strokeWidth=2;" edge="1" parent="1" source="p2_reflect" target="p2_host"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_start_e6" value="璇诲彇" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#70AD47;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=0;entryX=0.5;entryY=1;" edge="1" parent="1" source="p2_sqlite" target="p2_login"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_burn_layer" value="B. Plugin.BurnIn 鑰佸寲娴嬭瘯涓婚摼璺�" style="swimlane;horizontal=1;startSize=36;rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF9F0;strokeColor=#E6A23C;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=14;fontStyle=1;fontColor=#7F6000;" vertex="1" parent="1"><mxGeometry x="60" y="470" width="1560" height="655" as="geometry"/></mxCell>
+ <mxCell id="p2_burnin" value="<b>BurnInUI</b><br>鑰佸寲鐐�/绌翠綅缂栨帓<br>鍚姩銆佸仠姝€�佺姸鎬佷笌鍛婅" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FCE4D6;strokeColor=#ED7D31;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#843C0C;spacing=8;" vertex="1" parent="1"><mxGeometry x="90" y="545" width="260" height="115" as="geometry"/></mxCell>
+ <mxCell id="p2_engine" value="<b>ITestExecutionWrapper</b><br>鎸夊簭鍒楁墿灞曞悕閫夋嫨寮曟搸<br>.tapplan 鈫� OpenTAP" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#7F6000;spacing=8;" vertex="1" parent="1"><mxGeometry x="420" y="545" width="255" height="115" as="geometry"/></mxCell>
+ <mxCell id="p2_tap" value="<b>TapWrapper</b><br>TestPlanReader / Plan.Execute<br>StationGlobalsManager" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="725" y="505" width="230" height="95" as="geometry"/></mxCell>
+ <mxCell id="p2_ts" value="<b>TeststandWrapper</b><br>NI AxApplicationMgr<br>Sequence / StationGlobals" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#D6B656;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="725" y="625" width="230" height="95" as="geometry"/></mxCell>
+ <mxCell id="p2_msg" value="<b>UIMessage 浜嬩欢鎬荤嚎</b><br>EventCode 10014<br>鏂规硶鐮佹洿鏂扮姸鎬�/璁℃暟/鏇茬嚎/鎻愮ず" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E4DFEC;strokeColor=#8064A2;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#4C3768;spacing=8;" vertex="1" parent="1"><mxGeometry x="1005" y="545" width="250" height="115" as="geometry"/></mxCell>
+ <mxCell id="p2_cells" value="<b>DataTable / SingleCell</b><br>鐐変綋涓庣┐浣嶆ā鍨�<br>鐘舵�併�佹潯鐮併�佹祴閲忓�笺�佸浘琛ㄦ暟鎹�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E2F0D9;strokeColor=#70AD47;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#375623;spacing=8;" vertex="1" parent="1"><mxGeometry x="1310" y="545" width="260" height="115" as="geometry"/></mxCell>
+ <mxCell id="p2_config" value="<b>浜у搧涓庤繍琛岄厤缃�</b><br>ParameterManager / ProductManager<br>Scanner.ini / Count.ini / 涓诲簭鍒�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E7E6E6;strokeColor=#7F8C8D;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="90" y="760" width="260" height="100" as="geometry"/></mxCell>
+ <mxCell id="p2_plc" value="<b>PLCHelper.S7Context</b><br>BurnInControler<br>鐐変綋銆佺┐浣嶄笌鍐峰嵈鐘舵�佽鍐�" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="405" y="760" width="260" height="100" as="geometry"/></mxCell>
+ <mxCell id="p2_scan" value="<b>鎵爜涓� MES</b><br>SerialPort + 鏉$爜瑙f瀽<br>HTTP 鑾峰彇 PN / 杩囩珯鏍¢獙" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#DDEBF7;strokeColor=#5B9BD5;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="720" y="790" width="260" height="100" as="geometry"/></mxCell>
+ <mxCell id="p2_output" value="<b>缁撴灉涓庡彲瑙嗗寲</b><br>SQLite / CSV / Count.ini<br>LiveCharts 鏇茬嚎銆佹祴璇曠粨鏋滀笌鎶ヨ" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#E2F0D9;strokeColor=#70AD47;fontFamily=Microsoft YaHei;fontSize=11;spacing=8;" vertex="1" parent="1"><mxGeometry x="1310" y="760" width="260" height="100" as="geometry"/></mxCell>
+ <mxCell id="p2_ext_boundary" value="澶栭儴杩愯渚濊禆锛氳タ闂ㄥ瓙 PLC锝滀覆鍙f壂鐮佸櫒锝淢ES HTTP 鏈嶅姟锝淣I TestStand 鎴� OpenTAP 杩愯鏃躲�傚綋鍓嶆簮鐮佸揩鐓ф湭鍖呭惈 OpenTAP/UILib/UtilLib 澶栭儴婧愬伐绋嬶紝涓旀湭鍋氳澶囪仈鏈洪獙璇併��" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFF2CC;strokeColor=#C9A227;dashed=1;fontFamily=Microsoft YaHei;fontSize=11;fontColor=#7F6000;spacing=10;align=left;" vertex="1" parent="1"><mxGeometry x="90" y="985" width="1480" height="80" as="geometry"/></mxCell>
+ <mxCell id="p2_e1" value="鍒濆鍖栨墽琛岃鍥�" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#C65911;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_burnin" target="p2_engine"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e2" value="OpenTAP" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#C65911;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_engine" target="p2_tap"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e3" value="鍏朵粬搴忓垪" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#C65911;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_engine" target="p2_ts"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e4" value="缁熶竴浜嬩欢" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#8064A2;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_tap" target="p2_msg"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e5" value="缁熶竴浜嬩欢" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#8064A2;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_ts" target="p2_msg"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e6" value="鏇存柊妯″瀷" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#8064A2;strokeWidth=2;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;" edge="1" parent="1" source="p2_msg" target="p2_cells"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e7" value="钀藉簱/瀵煎嚭" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#70AD47;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p2_cells" target="p2_output"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e8" value="璇诲彇" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#808080;strokeWidth=1.5;dashed=1;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=0;entryX=0.5;entryY=1;" edge="1" parent="1" source="p2_config" target="p2_burnin"><mxGeometry relative="1" as="geometry"/></mxCell>
+ <mxCell id="p2_e9" value="鎺у埗/閲囬泦" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p2_burnin" target="p2_plc"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="220" y="720"/><mxPoint x="535" y="720"/></Array></mxGeometry></mxCell>
+ <mxCell id="p2_e10" value="鏉$爜/杩囩珯" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=1;strokeColor=#5B9BD5;strokeWidth=1.5;fontFamily=Microsoft YaHei;fontSize=10;labelBackgroundColor=#FFFFFF;exitX=0.5;exitY=1;entryX=0.5;entryY=0;" edge="1" parent="1" source="p2_burnin" target="p2_scan"><mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="220" y="735"/><mxPoint x="850" y="735"/></Array></mxGeometry></mxCell>
+ </root>
+ </mxGraphModel>
+ </diagram>
+</mxfile>
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01.drawio.png" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01.drawio.png"
new file mode 100644
index 0000000..5e23e47
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01.drawio.png"
Binary files differ
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01_\351\242\204\350\247\210.png" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01_\351\242\204\350\247\210.png"
new file mode 100644
index 0000000..9f5e0a5
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_01_\351\242\204\350\247\210.png"
Binary files differ
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02.drawio.png" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02.drawio.png"
new file mode 100644
index 0000000..36ee5e4
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02.drawio.png"
Binary files differ
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02_\351\242\204\350\247\210.png" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02_\351\242\204\350\247\210.png"
new file mode 100644
index 0000000..cb1f3e9
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204_02_\351\242\204\350\247\210.png"
Binary files differ
diff --git "a/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204\345\210\206\346\236\220.md" "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204\345\210\206\346\236\220.md"
new file mode 100644
index 0000000..ee163fc
--- /dev/null
+++ "b/docs/TapProject_\344\273\243\347\240\201\347\273\223\346\236\204\345\210\206\346\236\220.md"
@@ -0,0 +1,55 @@
+# TapProject 浠g爜缁撴瀯鍒嗘瀽
+
+## 1. 鍒嗘瀽鑼冨洿涓庣粨璁�
+
+- 鍒嗘瀽鐩綍锛歚D:\TestPlatform\TapProject`
+- 婧愮爜瑙勬ā锛�45 涓唴閮� `.csproj`銆�476 涓� C# 鏂囦欢銆�157 涓� XAML 鏂囦欢锛堝潎鎺掗櫎 `bin/obj/MigrationBackup`锛夈��
+- 鎶�鏈熀绾匡細鍏ㄩ儴闈㈠悜 .NET Framework 4.7.2锛涘叾涓� 33 涓棫寮忓伐绋嬩娇鐢� `v4.7.2`锛�12 涓� SDK 椋庢牸宸ョ▼浣跨敤 `net472`銆�
+- 鎬讳綋褰㈡�侊細杩欐槸涓�涓� WPF 妗岄潰娴嬭瘯骞冲彴锛岄噰鐢ㄢ�滀富瀹夸富 + 鍏叡 HMI 妗嗘灦 + 鏉冮檺椹卞姩鐨勫姩鎬佹彃浠� + 璁惧/娴嬭瘯鎵ц閫傞厤鍣ㄢ�濈殑妯″潡鍖栧崟浣撴灦鏋勩��
+- 鏍稿績涓氬姟锛歚Plugin.BurnIn` 璐熻矗鑰佸寲鐐�/绌翠綅 UI銆佹壂鐮併�丳LC 鐘舵�併�丮ES 鏍¢獙銆佹祴璇曟墽琛屾秷鎭鐞嗐�佹洸绾垮拰缁撴灉鎸佷箙鍖栵紱鎵ц寮曟搸閫氳繃 `ITestExecutionWrapper` 鍦� NI TestStand 涓� OpenTAP 涔嬮棿鍒囨崲銆�
+
+瀵瑰簲鍥捐 `TapProject_浠g爜缁撴瀯.drawio`锛�
+
+1. `01-鎬讳綋鍒嗗眰缁撴瀯`锛氬睍绀轰唬鐮佹ā鍧楀垎灞傘�佸叧閿緷璧栧拰澶栭儴绯荤粺銆�
+2. `02-杩愯鏃朵笌鑰佸寲涓婚摼璺痐锛氬睍绀哄惎鍔ㄣ�佺櫥褰曘�佹潈闄愯彍鍗曘�佸姩鎬佽杞姐�佹彃浠剁敓鍛藉懆鏈熶互鍙婅�佸寲娴嬭瘯鏁版嵁閾捐矾銆�
+
+## 2. 浠g爜妯″潡鍒嗗眰
+
+| 灞傜骇 | 涓昏宸ョ▼ | 鑱岃矗 |
+|---|---|---|
+| WPF 瀹夸富 | `WPFProject/TapProject.csproj` | 鍗曞疄渚嬪惎鍔ㄣ�佹棩蹇�/璇█/璧勬簮鍒濆鍖栥�佺櫥褰曘�佽杞借繘搴︺�佷富绐楀彛鍜岄〉绛炬壙杞� |
+| 鎻掍欢濂戠害涓庤杞� | `HMI.Core`銆乣HMI.Base` | `IPlugin`/`IPluginContainer` 鐢熷懡鍛ㄦ湡濂戠害銆乣PluginLoader`銆佹彃浠跺熀绫汇�佸弽灏勫吋瀹瑰寘瑁� |
+| 鍏叡 UI 涓庡伐鍏� | `HMI.UI`銆乣HMI.StdControl`銆乣HMI.Util`銆乣HMI.Util.Base`銆乣HMI.Simple.Util`銆乣HMI.LogUtil`銆乣HMI.SerialData.Util`銆乣HMI.MQUtil` | WPF 鎺т欢/鏍峰紡銆佺粦瀹氳浆鎹㈠櫒銆佽矾寰�/閰嶇疆/搴忓垪鍖栥�佹棩蹇椼�佷覆鍙c�丮SMQ 绛夐�氱敤鑳藉姏 |
+| 绯荤粺绠$悊 HMI | `HMI.MenuManage`銆乣HMI.SettingControl`銆乣HMI.ParameterManage`銆乣HMI.AlarmInfoManage`銆乣HMI.DataViewer`銆乣HMI.LogViewer` | 鐢ㄦ埛/瑙掕壊/鑿滃崟銆佺郴缁熻缃�佸弬鏁般�佹姤璀︺�佹暟鎹拰鏃ュ織鏌ヨ |
+| 鑰佸寲涓庤澶囦笟鍔℃彃浠� | `Plugin.BurnIn`銆乣Plugin.CurrentSensorControl`銆乣Plugin.DOControl`銆乣Plugin.DODriverControl`銆乣Plugin.ModelManage`銆乣Plugin.JarUI` | 鑰佸寲娴嬭瘯涓荤晫闈€�佺數娴�/DO 鎺у埗銆佸瀷鍙蜂笌宸ヨ鐣岄潰 |
+| 娴嬭瘯鎵ц涓庣紪杈� | `Plugin.TsExecution`銆乣Plugin.StandardTest`銆乣Plugin.TestPlanEditor`銆乣Plugin.SeqEditor`銆乣Plugin.TsSupport`銆乣HMI.TapApi`銆乣Util.ManualDebug` | TestStand/OpenTAP 鎶借薄銆佸簭鍒楁墽琛屻�佽鍒�/姝ラ缂栬緫銆佹墜鍔ㄨ皟璇� |
+| 鏁版嵁涓庢姤鍛婃彃浠� | `Plugin.TestResult`銆乣Plugin.DataViewer`銆乣Plugin.DataBoard`銆乣Plugin.QualityReport` | 娴嬭瘯缁撴灉銆佷笟鍔℃暟鎹�佺湅鏉裤�佽川閲忔姤琛� |
+| 璁惧涓庨�氫俊閫傞厤 | `PLCHelper`銆乣S7Driver`銆乣OPCClient`銆乣ModBusTcpController`銆乣DIOController`銆乣ConnectionUtil`銆乣CellStepSender` | S7 PLC銆丱PC銆丮odbus TCP銆丏IO銆乀CP/姝ラ鍙戦�佺瓑搴曞眰鎺ュ叆 |
+| 杈呭姪绋嬪簭 | `HMI.UnitTest`銆乣ConsoleTest`銆乣StationGlobalsReader` | 闆嗘垚娴嬭瘯澹炽�佹帶鍒跺櫒鎺у埗鍙拌瘯楠屻�乀estStand StationGlobals 璇诲彇 |
+
+## 3. 杩愯鏃朵富閾捐矾
+
+1. `App.Application_Startup` 鎵ц鍗曞疄渚嬩繚鎶わ紝娉ㄥ唽鍏煎缁勪欢锛屽垵濮嬪寲鏃ュ織銆佽瑷�銆佺郴缁熻缃拰 Toast锛岀劧鍚庢樉绀虹櫥褰曠獥鍙o紙`WPFProject/App.xaml.cs:94-119`锛夈��
+2. `Login` 浠� SQLite 鐨� `user / role / user_role_rel / role_menu_rel / menu` 琛ㄨ鍙栫敤鎴蜂笌鏉冮檺鑿滃崟锛屽苟鍐欏叆鍏ㄥ眬 `UserInfo`锛坄WPFProject/Login.xaml.cs:59-130`锛夈��
+3. `LoadingWindow` 鍒濆鍖栨姤璀︽湇鍔″苟璋冪敤 `PluginLoader.LoadMenuAndPlugin`锛坄WPFProject/LoadingWindow.xaml.cs:107-118`锛夈��
+4. `PluginLoader` 鏍规嵁褰撳墠鐢ㄦ埛鐨� `AllPermissions` 鎷煎嚭鑿滃崟鏍戯紝浠� `Plugin` 鐩綍鍔犺浇 DLL锛屾寜鏁版嵁搴撲腑鐨� `className` 鍙嶅皠鍒涘缓瀹炰緥锛屾敞鍏ユ棩蹇楀拰鎻掍欢绉佹湁閰嶇疆鐩綍锛岃皟鐢� `InitFinish` 骞舵寕鎺ュ叧闂簨浠讹紙`HMI.Base/PluginLoader.cs:52-123`锛夈��
+5. `PanuonWindow` 鏄� `IPluginContainer`锛沗MainTabControl` 鍦ㄥ垏鎹㈣彍鍗曟椂璋冪敤鎻掍欢鐨� `CanDeActive / DeActive / CanActive / Active`锛屾妸 WPF 鎴� WinForms 鍐呭瑁呭叆瀹夸富椤碉紙`WPFProject/Controls/MainTabControl.xaml.cs:138-181`锛夈��
+
+杩欐剰鍛崇潃鑿滃崟銆佹潈闄愬拰鎻掍欢绫诲瀷骞堕潪鍙敱缂栬瘧寮曠敤鍐冲畾锛涢儴缃茬洰褰曚腑鐨� DLL銆丼QLite 鑿滃崟鏁版嵁鍙婄敤鎴疯鑹插叡鍚屽喅瀹氬疄闄呰繍琛屾椂鐣岄潰銆�
+
+## 4. 鑰佸寲涓氬姟涓婚摼璺�
+
+- `Plugin.BurnIn.BurnInUI` 缁ф壙 `PluginMaskControl`锛屽垵濮嬪寲 S7 PLC 涓婁笅鏂囷紝璇诲彇浜у搧/鍏叡鍙傛暟銆佹壂鐮侀厤缃拰涓绘祴璇曞簭鍒楋紙`Plugin.BurnIn/BurnInUI.xaml.cs:25,105,179-222`锛夈��
+- 搴忓垪鎵╁睍鍚嶄负 `.tapplan` 鏃堕�夋嫨 `TapWrapper`锛屽惁鍒欓�夋嫨 `TeststandWrapper`锛涗袱鑰呴兘瀹炵幇 `ITestExecutionWrapper`锛屽悜涓婄粺涓�鎻愪緵搴忓垪鍚姩銆丼tationGlobals/FileGlobals 鍜� UI 娑堟伅锛坄Plugin.BurnIn/TestResultView.xaml.cs:209-263`銆乣Plugin.TsSupport/ITestExecutionWrapper.cs`锛夈��
+- 鎵ц寮曟搸浠ヤ簨浠剁爜 `10014` 鍜屾柟娉曠爜椹卞姩鐐変綋/绌翠綅鐘舵�併�佽鏁般�佹洸绾裤�佹彁绀哄拰缁撴灉淇濆瓨锛涚粨鏋滃彲钀藉湴鍒� SQLite 涓� CSV锛坄Plugin.BurnIn/TestResultView.xaml.cs:300-603`銆乣Plugin.BurnIn/SingleCell.xaml.cs:950-1030`锛夈��
+- `Scanner` 閫氳繃涓插彛璇荤爜锛屼笟鍔″弬鏁板喅瀹� MES 绔欎綅锛沗PNUtil` 閫氳繃 HTTP 璋冪敤 MES 鑾峰彇 PN 鍜岃繃绔欐牎楠岋紙`Plugin.BurnIn/Scanner.xaml.cs`銆乣Plugin.BurnIn/Util/PNUtil.cs:27-84`锛夈��
+- `BurnInUI` 涓� `SingleCell/Cooler` 閫氳繃 `PLCHelper.S7Context` 璇诲啓 `BurnInControler` PLC锛坄Plugin.BurnIn/Util/RunTimeVariablePool.cs`銆乣Plugin.BurnIn/SingleCell.xaml.cs:1107-1124`锛夈��
+
+## 5. 閲嶈杈圭晫涓庨闄�
+
+1. **褰撳墠妫�鍑烘棤娉曠嫭绔嬪畬鏁寸紪璇戙��** 闈欐�佽В鏋愬彂鐜� 17 鏉℃湭瑙f瀽 `ProjectReference`锛屾秹鍙� 7 涓伐绋嬶紱缂哄け婧愭爲鍖呮嫭 `D:\Project\OpenTapEditor`銆乣D:\TestPlatform\OpenTapEditor` 涓� `D:\VisualStudio\UILib/UtilLib`銆傚浘涓妸瀹冧滑鏍囦负鈥滃閮�/褰撳墠缂哄け鈥濓紝鑰屼笉鏄湰浠撳簱宸插叿澶囩殑妯″潡銆�
+2. **鎻掍欢鍖栧苟闈炲畬鍏ㄨВ鑰︺��** 涓诲伐绋嬪悓鏃剁洿鎺ュ紩鐢ㄥぇ閲忔彃浠堕」鐩紝鍙堝湪杩愯鏃朵緷鎹暟鎹簱鍙嶅皠鍔犺浇鎻掍欢 DLL锛涜繖鏄�滅紪璇戞湡鑱氬悎 + 杩愯鏃惰彍鍗曟彃浠跺寲鈥濈殑娣峰悎妯″紡銆�
+3. **鏍蜂緥鑿滃崟搴撲笌婧愮爜瀛樺湪婕傜Щ銆�** `WPFProject/Config/Database/database.db` 鐨� `menu` 琛ㄥ綋鍓嶆湁 13 鏉¤褰曪紙2 涓竴绾х洰褰曘��11 涓彾瀛愰」锛夛紝鍏朵腑寮曠敤 `HMI.IOMonitor.dll`锛屼絾婧愮爜鐩綍涓嶅瓨鍦紱鍚屾椂 `Plugin.BurnIn` 绛夊凡缂栬瘧宸ョ▼鏈嚭鐜板湪璇ユ牱渚嬭彍鍗曡〃涓�傞儴缃茬増鏁版嵁搴撳彲鑳戒笉鍚岋紝涓嶈兘鎶婃牱渚嬪簱瑙嗕负鏈�缁堝姛鑳芥竻鍗曘��
+4. **杩愯鐩綍鏄殣寮忎緷璧栥��** `PathHelper` 鐢� `Path.GetFullPath(".")` 浣滀负鏍圭洰褰曪紝`Plugin/Config/Icon/Lang` 绛夎矾寰勫彇鍐充簬杩涚▼宸ヤ綔鐩綍锛岃�屼笉瀹屽叏鍙栧喅浜� EXE 鎵�鍦ㄧ洰褰曘��
+5. 鏈鏄潤鎬佷唬鐮佷笌閰嶇疆缁撴瀯鍒嗘瀽锛屾湭鎵ц瀹屾暣缂栬瘧鍜岃澶囪仈鏈洪獙璇侊紱鍥句腑鐨勫璁惧叧绯绘潵鑷唬鐮佽皟鐢ㄤ笌椤圭洰寮曠敤锛屼笉浠h〃鐜板満绔彛銆佸崗璁弬鏁版垨璁稿彲宸查獙璇併��
+
diff --git a/fix_menu.py b/fix_menu.py
new file mode 100644
index 0000000..7cc224f
--- /dev/null
+++ b/fix_menu.py
@@ -0,0 +1,42 @@
+import sqlite3
+
+# Fix output database
+out_db = r'd:\TestPlatform\TapProject\WPFProject\bin\x64\Debug\Config\Database\database.db'
+conn = sqlite3.connect(out_db)
+c = conn.cursor()
+
+# Check existing child menu entries for reference
+c.execute("SELECT code, name, iconPath, level FROM menu WHERE parentCode='ManualControl' AND code NOT IN ('DeviceConfig','DeviceDebug')")
+print("Reference entries:")
+for r in c.fetchall():
+ print(f" {r}")
+
+# Fix: set iconPath and level to match existing pattern
+c.execute("""UPDATE menu SET iconPath = '.\\xinzeng_1.png', level = '2'
+ WHERE code IN ('DeviceConfig','DeviceDebug')""")
+conn.commit()
+
+# Verify
+c.execute("SELECT code, name, iconPath, level, type, parentCode FROM menu WHERE code IN ('DeviceConfig','DeviceDebug')")
+print("\nFixed entries:")
+for r in c.fetchall():
+ print(f" {r}")
+
+conn.close()
+
+# Fix source database
+src_db = r'd:\TestPlatform\TapProject\WPFProject\Config\Database\database.db'
+conn = sqlite3.connect(src_db)
+c = conn.cursor()
+
+c.execute("""UPDATE menu SET level = '2'
+ WHERE code IN ('DeviceConfig','DeviceDebug')""")
+conn.commit()
+
+c.execute("SELECT code, name, iconPath, level FROM menu WHERE code IN ('DeviceConfig','DeviceDebug')")
+print("\nSource DB fixed:")
+for r in c.fetchall():
+ print(f" {r}")
+
+conn.close()
+print("\nDone!")
--
Gitblit v1.9.1