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; using Aging.Infrastructure.Contracts; using Aging.Infrastructure.Drivers; using Aging.Infrastructure.Services; using CanSimulator; using HMI.Core.Util; 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; // V3.0:DeviceBroker 宿主子进程(存在 Broker 设备时自动拉起) private System.Diagnostics.Process? _brokerProcess; // 自动化测试服务 private IAutoDeviceFacade? _autoFacade; private ICanBusService? _canBusService; private CancellationTokenSource? _autoCts; public MainWindow() { InitializeComponent(); } private void Window_Loaded(object sender, RoutedEventArgs e) { InitializePlugins(); InitializeCanSimulator(); } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { // 取消正在运行的自动化测试 try { _autoCts?.Cancel(); } catch { } // 停止 DeviceBroker 宿主(其持有的物理会话随进程退出释放,租约靠 TTL 过期+人工回收) try { if (_brokerProcess != null && !_brokerProcess.HasExited) { _brokerProcess.Kill(); _brokerProcess.WaitForExit(3000); } _brokerProcess?.Dispose(); } catch { } // CAN 模拟器已外部化为独立服务进程,本进程无进程内总线需清理 // 不删除测试目录,保留数据以便重启后恢复 // (如需手动重置,使用工具栏中的 "重置数据" 按钮) } /// /// 初始化两个插件并嵌入到对应 Tab 中 /// 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 { // 初始化仪器仪表注册插件(建库 data.db) _deviceConfigPlugin = new Plugin.DeviceConfig.DeviceConfig(); _deviceConfigPlugin.SetBaseFolder(_testDir); _deviceConfigPlugin.SetLogger(_testLog); _deviceConfigPlugin.InitFinish(); deviceConfigHost.Child = _deviceConfigPlugin; // 预置 CAN 测试设备(必须在基础设施初始化前写入,Bootstrapper 据此注册代理驱动) SeedPresetCanDevices(); // 初始化基础设施 & 自动化服务(必须在 DeviceDebug.InitFinish 之前: // 它从 UtilContainer 拉取 DeviceManager/ResourceManager,注册在 Bootstrapper.Initialize 完成) InitializeAutoServices(); // 初始化硬件调试插件(此时基础设施服务已就绪,维护租约/驱动复用链路可用) _deviceDebugPlugin = new Plugin.DeviceDebug.DeviceDebug(); _deviceDebugPlugin.SetBaseFolder(_testDir); _deviceDebugPlugin.SetLogger(_testLog); _deviceDebugPlugin.InitFinish(); deviceDebugHost.Child = _deviceDebugPlugin; // 刷新插件内部列表(需手动调用插件方法) _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,添加设备、通道、Slot映射等"); 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. 切换到 '全自动测试' Tab 可体验方案 B+C 的自动化步骤"); Log(" 6. 可随时点击 '运行自动化测试' 验证核心逻辑"); } catch (Exception ex) { txtStatus.Text = $"插件初始化失败: {ex.Message}"; Log($"[ERROR] 插件初始化失败: {ex.Message}"); Log(ex.StackTrace ?? ""); } } /// /// 预置 CAN 测试设备(仅当不存在时插入,避免覆盖用户修改) /// 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}"); } } /// /// 重置测试数据(删除本地目录中的 db 文件) /// 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条Slot映射"); 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, "版本状态应为Draft"); Assert(!string.IsNullOrEmpty(versions[0].Hash), "哈希不应为空"); }); TestStep("冲突检测(应无冲突)", () => { var conflicts = configModel.DetectConflicts(); Assert(conflicts.Count == 0, $"不应有冲突,实际: {string.Join("; ", conflicts)}"); }); TestStep("添加第二个设备并检测IP冲突", () => { 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("打开调试会话(ReadOnly)", () => { var session = sessionMgr.OpenSession("PS-001", "OP001", "张工", "V2.0"); sessionId = session.SessionId; Assert(!string.IsNullOrEmpty(sessionId), "会话ID不应为空"); Assert(session.State == DebugSessionState.ReadOnly, "初始状态应为ReadOnly"); Assert(session.DeviceId == "PS-001", "设备ID应匹配"); Log($" 会话ID: {sessionId}"); }); if (sessionId == null) { SkipRemainingSection(6, "打开会话失败"); } else { TestStep("请求维护模式(ReadOnly → AcquiringMaintenance)", () => { bool result = sessionMgr.RequestMaintenance(sessionId!); Assert(result, "请求维护应成功"); var state = sessionMgr.GetSessionState(sessionId!); Assert(state == DebugSessionState.AcquiringMaintenance, $"状态应为AcquiringMaintenance,实际{state}"); }); TestStep("激活维护模式(AcquiringMaintenance → MaintenanceActive)", () => { sessionMgr.ActivateMaintenance(sessionId!); var state = sessionMgr.GetSessionState(sessionId!); Assert(state == DebugSessionState.MaintenanceActive, $"状态应为MaintenanceActive,实际{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("关闭会话(MaintenanceActive → Closing → Closed)", () => { sessionMgr.CloseSession(sessionId!); var state = sessionMgr.GetSessionState(sessionId!); Assert(state == DebugSessionState.Closed, $"状态应为Closed,实际{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, $"状态应为Faulted,实际{state}"); sessionMgr.MarkRecoveryRequired(faultId); state = sessionMgr.GetSessionState(faultId); Assert(state == DebugSessionState.RecoveryRequired, $"状态应为RecoveryRequired,实际{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, "哈希计算流程正常"); }); 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 全自动测试(方案 B+C) /// /// 初始化自动化服务: /// 1) 启动 Infrastructure Bootstrapper(ResourceManager/DeviceManager/DeviceService/AutoDeviceFacade/CanBusService) /// 2) 为预置 CAN 设备注册驱动 /// 3) 将设备列表填到 UI 下拉框 /// private void InitializeAutoServices() { try { // 初始化基础设施(幂等,已初始化时自动跳过) string dbPath = Path.Combine(_testDir, "data.db"); try { InfrastructureBootstrapper.Initialize(dbPath); } catch (Exception ex) { Log($"[WARN] Bootstrapper 初始化异常: {ex.Message}"); } // 从 UtilContainer 拉取服务实例 var container = new UtilContainer(); _autoFacade = container.GetUtil("AutoDeviceFacade"); _canBusService = container.GetUtil("CanBusService"); // 为预置设备注册驱动(自动启动 CAN 总线) RegisterCanDriver("CAN-001", "CAN_BUS_1:0"); RegisterCanDriver("CAN-002", "CAN_BUS_1:1"); // 初始化设备下拉列表 autoTxDeviceCombo.ItemsSource = new[] { "CAN-001", "CAN-002" }; autoRxDeviceCombo.ItemsSource = new[] { "CAN-002", "CAN-001" }; autoTxDeviceCombo.SelectedIndex = 0; autoRxDeviceCombo.SelectedIndex = 0; if (_autoFacade != null && _canBusService != null) { Log("[INFO] AutoDeviceFacade 和 CanBusService 已就绪"); Log("[INFO] CAN-001 / CAN-002 驱动已注册"); } else { Log("[ERROR] 自动化服务未正确初始化,全自动测试 Tab 不可用"); autoRunBtn.IsEnabled = false; } // V3.0:若配置库存在 Broker 域设备,自动拉起 DeviceBroker.exe 宿主 TryStartDeviceBroker(); } catch (Exception ex) { Log($"[ERROR] 初始化自动化服务失败: {ex.Message}"); Log(ex.StackTrace ?? ""); autoRunBtn.IsEnabled = false; } } /// /// V3.0:若 data.db 中存在 ExecutionMode=Broker 的设备,自动拉起 DeviceBroker.exe 宿主子进程。 /// 宿主默认参数指向本环境同一 data.db/arb.db(同机仲裁域);找不到可执行文件时降级为警告, /// Broker 设备将报 BROKER_UNREACHABLE,Local 设备不受影响。 /// private void TryStartDeviceBroker() { try { var model = new DeviceConfigModel(); string dbPath = Path.Combine(_testDir, "data.db"); if (!File.Exists(dbPath)) return; model.InitDatabase(dbPath); var brokerDevices = model.GetAllDevices() .Where(d => d.ExecutionMode == Plugin.DeviceConfig.Entity.ExecutionModeKind.Broker) .ToList(); if (brokerDevices.Count == 0) { Log("[INFO] 无 Broker 域设备,未启动 DeviceBroker 宿主"); return; } string exe = FindDeviceBrokerExe(); if (exe == null) { Log("[WARN] 未找到 DeviceBroker.exe,Broker 设备链路不可用(请构建 DeviceBroker 工程)"); return; } var psi = new System.Diagnostics.ProcessStartInfo(exe) { Arguments = $"--db \"{dbPath}\" --arb \"{Path.Combine(_testDir, "arb.db")}\" --broker broker-b01", UseShellExecute = false, CreateNoWindow = true, }; _brokerProcess = System.Diagnostics.Process.Start(psi); Log($"[INFO] DeviceBroker 宿主已启动 (PID={_brokerProcess?.Id}),Broker 设备: " + string.Join(", ", brokerDevices.Select(d => d.DeviceId))); } catch (Exception ex) { Log($"[WARN] 启动 DeviceBroker 失败: {ex.Message}"); } } private static string FindDeviceBrokerExe() { string baseDir = AppDomain.CurrentDomain.BaseDirectory; var candidates = new[] { Path.Combine(baseDir, "DeviceBroker.exe"), Path.Combine(baseDir, @"DeviceBroker\DeviceBroker.exe"), Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\..\DeviceBroker\bin\Debug\net472\DeviceBroker.exe")), Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\DeviceBroker\bin\Debug\net472\DeviceBroker.exe")), }; foreach (var c in candidates) { try { if (File.Exists(c)) return c; } catch { } } return null; } /// /// 向 DeviceManager 注册 CAN 驱动(使用 DriverFactory) /// private void RegisterCanDriver(string deviceId, string endpoint) { try { InfrastructureBootstrapper.RegisterDevice(deviceId, "CAN", endpoint); } catch (Exception ex) { Log($"[WARN] 注册设备 {deviceId} 驱动失败: {ex.Message}"); } } /// /// 运行自动化步骤(封装完整流程:开总线 → 租约 → 发送 → 回收 → 释放) /// private async void AutoRunBtn_Click(object sender, RoutedEventArgs e) { if (_autoFacade == null || _canBusService == null) { AutoLog("[ERROR] 服务未初始化"); return; } string txDevice = autoTxDeviceCombo.SelectedItem as string ?? "CAN-001"; string rxDevice = autoRxDeviceCombo.SelectedItem as string ?? "CAN-002"; if (!int.TryParse(autoLoopCount.Text, out int loops) || loops <= 0) { AutoLog("[ERROR] 循环次数必须为正整数"); return; } if (!int.TryParse(autoInterval.Text, out int intervalMs) || intervalMs < 0) { intervalMs = 200; } // 确保总线已启动(使用 CAN-001 驱动时会自动启动) EnsureBusRunning(); // 启用中止按钮 autoRunBtn.IsEnabled = false; autoStopBtn.IsEnabled = true; autoProgress.Maximum = loops; autoProgress.Value = 0; _autoCts = new CancellationTokenSource(); var ct = _autoCts.Token; AutoLog("══════════════════════════════════════════════════════"); AutoLog($" CAN 老化自动化步骤开始"); AutoLog($" 发送设备: {txDevice}, 目标设备: {rxDevice}"); AutoLog($" 循环次数: {loops}, 帧间隔: {intervalMs}ms"); AutoLog("══════════════════════════════════════════════════════"); int successCount = 0; int failCount = 0; var swTotal = System.Diagnostics.Stopwatch.StartNew(); // ===== 方案 B:作用域租约(一次申请,多次使用) ===== try { using var lease = await _autoFacade.AcquireAsync(txDevice, "CAN_Aging", ct); AutoLog($"[租约] 已获得 {txDevice} 作用域租约: Session={lease.SessionId.Substring(0, 8)}, 时刻 {lease.AcquiredAt:HH:mm:ss.fff}"); // ===== 方案 C:使用类型化服务 ===== // 目标设备先启用自动响应(以便回环验证) using var rxLease = await _autoFacade.AcquireAsync(rxDevice, "CAN_Aging_Rx", ct); AutoLog($"[租约] 已获得 {rxDevice} 作用域租约: Session={rxLease.SessionId.Substring(0, 8)}"); await _canBusService.SetAutoResponseAsync(rxLease, true, ct); AutoLog($"[配置] 已为 {rxDevice} 启用自动响应模式"); for (int i = 0; i < loops; i++) { if (ct.IsCancellationRequested) break; uint frameId = (uint)(0x100 + i); byte[] payload = new byte[] { (byte)i, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x5A }; // 发送(走 txDevice 租约) bool sent = await _canBusService.SendFrameAsync(lease, frameId, payload, ct); AutoLog($"[T+{i,3:D3}] TX {frameId:X3} | {BitConverter.ToString(payload).Replace("-", " ")} {(sent ? "✓" : "✗")}"); // 接收回响(走 rxDevice 租约) await Task.Delay(intervalMs, ct); var echo = await _canBusService.ReceiveFrameAsync(rxLease, ct); if (echo != null) { AutoLog($"[R+{i,3:D3}] RX {echo.FrameIdHex} | {echo.DataHex} 匹配={(echo.FrameId == frameId ? "✓" : "✗")}"); if (echo.FrameId == frameId && echo.Data.SequenceEqual(payload)) successCount++; else failCount++; } else { AutoLog($"[R+{i,3:D3}] RX 无回响"); failCount++; } autoProgress.Value = i + 1; autoProgressText.Text = $"{i + 1}/{loops} 完成"; } // 收尾:关闭自动响应 await _canBusService.SetAutoResponseAsync(rxLease, false, ct); AutoLog($"[配置] 已为 {rxDevice} 关闭自动响应模式"); } catch (OperationCanceledException) { AutoLog("[中止] 用户手动中止测试"); } catch (TimeoutException ex) { AutoLog($"[失败] 租约申请超时: {ex.Message}"); failCount++; } catch (InvalidOperationException ex) { AutoLog($"[失败] 资源冲突: {ex.Message}"); failCount++; } catch (Exception ex) { AutoLog($"[异常] {ex.GetType().Name}: {ex.Message}"); failCount++; } finally { swTotal.Stop(); // 使用 lease.Dispose() 会自动释放租约,无需手动写代码 _autoCts?.Dispose(); _autoCts = null; AutoLog(""); AutoLog("══════════════════════════════════════════════════════"); AutoLog($" 步骤完成 | 成功 {successCount} | 失败 {failCount} | 总耗时 {swTotal.ElapsedMilliseconds}ms"); AutoLog($" 租约已自动释放(using 作用域结束)"); AutoLog("══════════════════════════════════════════════════════"); autoRunBtn.IsEnabled = true; autoStopBtn.IsEnabled = false; autoProgressText.Text = "就绪"; } } /// /// 全自动·逐设备一次收发:模拟 TestPlan 的 TestStep,对 CAN-001~004 逐一 /// 通过类型化服务/门面调用 CAN 驱动,完成“发送命令→接收回显”一次完整往返。 /// 依赖总线应答器(BUS_ECHO)使单设备自发自收,无需对端设备。 /// private async void PerDeviceCanTest_Click(object sender, RoutedEventArgs e) { if (_autoFacade == null || _canBusService == null) { AutoLog("[ERROR] 服务未初始化"); return; } var devices = new[] { "CAN-001", "CAN-002", "CAN-003", "CAN-004" }; perDeviceBtn.IsEnabled = false; autoStopBtn.IsEnabled = true; _autoCts = new CancellationTokenSource(); var ct = _autoCts.Token; autoProgress.Maximum = devices.Length * 3 + 2; autoProgress.Value = 0; int passed = 0, failed = 0; var sw = System.Diagnostics.Stopwatch.StartNew(); AutoLog("════════════════════════════════════════════════"); AutoLog(" 全自动·逐设备一次收发(模拟 TestPlan TestStep)"); AutoLog("════════════════════════════════════════════════"); // 全局前置:启用总线应答器(使每次发送都能收到回显) await RunStepAsync("全局", "启用总线应答器 BUS_ECHO:ON", async () => (await _autoFacade.QueryAsync(devices[0], "BUS_ECHO:ON", ct)).Success, ok => { if (ok) passed++; else failed++; autoProgress.Value++; }); for (int idx = 0; idx < devices.Length; idx++) { if (ct.IsCancellationRequested) break; string dev = devices[idx]; uint frameId = (uint)(0x300 + idx); byte[] payload = new byte[] { (byte)idx, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 }; // TestStep 1:发送命令帧(OneShot:内部 预约→执行→释放) await RunStepAsync(dev, $"发送 0x{frameId:X3}", async () => await _canBusService.SendFrameAsync(dev, frameId, payload, ct), ok => { if (ok) passed++; else failed++; autoProgress.Value++; }); // TestStep 2:触发宿主 drain(Broker 设备回显经此捎带回客户端缓冲) await RunStepAsync(dev, "触发回显捎带(STATUS)", async () => (await _autoFacade.QueryAsync(dev, "STATUS", ct)).Success, ok => { if (ok) passed++; else failed++; autoProgress.Value++; }); // TestStep 3:接收回显并校验帧 ID await RunStepAsync(dev, "接收并校验回显", async () => { for (int t = 0; t < 10; t++) { var recv = await _canBusService.ReceiveFrameAsync(dev, ct); if (recv != null) { bool match = recv.FrameId == frameId; AutoLog($" [{dev}] RX {recv.FrameIdHex} | {recv.DataHex} 匹配={(match ? "✓" : "✗")}"); return match; } await Task.Delay(80, ct); } AutoLog($" [{dev}] 未收到回显"); return false; }, ok => { if (ok) passed++; else failed++; autoProgress.Value++; }); } // 全局收尾:关闭总线应答器 await RunStepAsync("全局", "关闭总线应答器 BUS_ECHO:OFF", async () => (await _autoFacade.QueryAsync(devices[0], "BUS_ECHO:OFF", ct)).Success, ok => { if (ok) passed++; else failed++; autoProgress.Value++; }); sw.Stop(); AutoLog("──────────────────────────────────────────────"); AutoLog($" 完成 | 通过 {passed} | 失败 {failed} | 耗时 {sw.ElapsedMilliseconds}ms"); AutoLog("════════════════════════════════════════════════"); autoProgressText.Text = $"通过 {passed} / 失败 {failed}"; perDeviceBtn.IsEnabled = true; autoStopBtn.IsEnabled = false; _autoCts?.Dispose(); _autoCts = null; } /// 模拟 TestPlan 的 TestStep:命名步骤 + 断言 + 记录通过/失败。 private async Task RunStepAsync(string device, string name, Func> body, Action onDone) { AutoLog($"▶ [TestStep] {device} :: {name}"); bool ok; try { ok = await body(); } catch (OperationCanceledException) { AutoLog(" [中止] 用户取消"); throw; } catch (Exception ex) { ok = false; AutoLog($" 异常: {ex.Message}"); } AutoLog($" {(ok ? "PASS ✓" : "FAIL ✗")}"); onDone(ok); } /// /// 中止当前自动化测试 /// private void AutoStopBtn_Click(object sender, RoutedEventArgs e) { _autoCts?.Cancel(); AutoLog("[中止] 已发送取消请求..."); } /// /// 清空自动化日志 /// private void AutoClearLog_Click(object sender, RoutedEventArgs e) { autoLog.Clear(); } /// /// 总线由外部 CanSimulatorServer 托管,本地无需再启动进程内总线(保留空实现兼容旧调用)。 /// private void EnsureBusRunning() { } /// /// 向自动化日志区追加日志 /// private void AutoLog(string msg) { string line = $"{DateTime.Now:HH:mm:ss.fff} {msg}"; Dispatcher.Invoke(() => { autoLog.AppendText(line + Environment.NewLine); autoLog.ScrollToEnd(); }); } #endregion #region CAN 模拟器 /// /// 初始化 CAN 模拟器 /// 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}"); } } /// /// 启动总线按钮点击 /// 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; } // 总线由外部模拟器服务托管:此处仅验证一个节点可接入(连通性探测) var driver = DriverFactory.CreateDriver("CAN_SIM_0", "CAN", $"{busName}:0"); driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult(); // 更新 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); } } /// /// 停止总线按钮点击 /// 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}"); } } /// /// 重置所有按钮点击 /// private void CanResetBtn_Click(object sender, RoutedEventArgs e) { try { _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(string name, Func 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 } }