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
{
// 检查是否已有 DeviceBroker 进程在运行(避免多实例争抢管道/arb.db)
if (System.Diagnostics.Process.GetProcessesByName("DeviceBroker").Length > 0)
{
Log("[INFO] DeviceBroker 宿主已在运行,跳过启动");
return;
}
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
}
}