wenxiangchao
4 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
Plugin.IntegrationTest/MainWindow.xaml.cs
@@ -8,8 +8,12 @@
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;
@@ -38,6 +42,14 @@
        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();
@@ -51,9 +63,22 @@
        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            // 清理 CAN 模拟器
            _canBus?.Dispose();
            CanDriverAdapter.ResetAllBuses();
            // 取消正在运行的自动化测试
            try { _autoCts?.Cancel(); } catch { }
            // 停止 DeviceBroker 宿主(其持有的物理会话随进程退出释放,租约靠 TTL 过期+人工回收)
            try
            {
                if (_brokerProcess != null && !_brokerProcess.HasExited)
                {
                    _brokerProcess.Kill();
                    _brokerProcess.WaitForExit(3000);
                }
                _brokerProcess?.Dispose();
            }
            catch { }
            // CAN 模拟器已外部化为独立服务进程,本进程无进程内总线需清理
            // 不删除测试目录,保留数据以便重启后恢复
            // (如需手动重置,使用工具栏中的 "重置数据" 按钮)
@@ -74,22 +99,26 @@
            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;
                // 预置 CAN 测试设备(方便直接验证驱动与面板)
                SeedPresetCanDevices();
                // 刷新插件内部列表(需手动调用插件方法)
                _deviceConfigPlugin.ForceRefreshForTest();
@@ -107,7 +136,8 @@
                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. 可随时点击 '运行自动化测试' 验证核心逻辑");
                Log("  5. 切换到 '全自动测试' Tab 可体验方案 B+C 的自动化步骤");
                Log("  6. 可随时点击 '运行自动化测试' 验证核心逻辑");
            }
            catch (Exception ex)
            {
@@ -628,6 +658,402 @@
            }
        }
        #region 全自动测试(方案 B+C)
        /// <summary>
        /// 初始化自动化服务:
        ///   1) 启动 Infrastructure Bootstrapper(ResourceManager/DeviceManager/DeviceService/AutoDeviceFacade/CanBusService)
        ///   2) 为预置 CAN 设备注册驱动
        ///   3) 将设备列表填到 UI 下拉框
        /// </summary>
        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<IAutoDeviceFacade>("AutoDeviceFacade");
                _canBusService = container.GetUtil<ICanBusService>("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;
            }
        }
        /// <summary>
        /// V3.0:若 data.db 中存在 ExecutionMode=Broker 的设备,自动拉起 DeviceBroker.exe 宿主子进程。
        /// 宿主默认参数指向本环境同一 data.db/arb.db(同机仲裁域);找不到可执行文件时降级为警告,
        /// Broker 设备将报 BROKER_UNREACHABLE,Local 设备不受影响。
        /// </summary>
        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;
        }
        /// <summary>
        /// 向 DeviceManager 注册 CAN 驱动(使用 DriverFactory)
        /// </summary>
        private void RegisterCanDriver(string deviceId, string endpoint)
        {
            try
            {
                InfrastructureBootstrapper.RegisterDevice(deviceId, "CAN", endpoint);
            }
            catch (Exception ex)
            {
                Log($"[WARN] 注册设备 {deviceId} 驱动失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 运行自动化步骤(封装完整流程:开总线 → 租约 → 发送 → 回收 → 释放)
        /// </summary>
        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 = "就绪";
            }
        }
        /// <summary>
        /// 全自动·逐设备一次收发:模拟 TestPlan 的 TestStep,对 CAN-001~004 逐一
        /// 通过类型化服务/门面调用 CAN 驱动,完成“发送命令→接收回显”一次完整往返。
        /// 依赖总线应答器(BUS_ECHO)使单设备自发自收,无需对端设备。
        /// </summary>
        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;
        }
        /// <summary>模拟 TestPlan 的 TestStep:命名步骤 + 断言 + 记录通过/失败。</summary>
        private async Task RunStepAsync(string device, string name, Func<Task<bool>> body, Action<bool> 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);
        }
        /// <summary>
        /// 中止当前自动化测试
        /// </summary>
        private void AutoStopBtn_Click(object sender, RoutedEventArgs e)
        {
            _autoCts?.Cancel();
            AutoLog("[中止] 已发送取消请求...");
        }
        /// <summary>
        /// 清空自动化日志
        /// </summary>
        private void AutoClearLog_Click(object sender, RoutedEventArgs e)
        {
            autoLog.Clear();
        }
        /// <summary>
        /// 总线由外部 CanSimulatorServer 托管,本地无需再启动进程内总线(保留空实现兼容旧调用)。
        /// </summary>
        private void EnsureBusRunning() { }
        /// <summary>
        /// 向自动化日志区追加日志
        /// </summary>
        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 模拟器
        /// <summary>
@@ -663,25 +1089,17 @@
                    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;
                    // 更新 UI
                    canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
                    canBusStatusText.Text = $"运行中 ({busName})";
                    canStartBtn.IsEnabled = false;
                    canStopBtn.IsEnabled = true;
                    Log($"[CAN] 总线 {busName} 已启动");
                }
                Log($"[CAN] 已连接外部模拟器总线 {busName}");
            }
            catch (Exception ex)
            {
@@ -720,7 +1138,6 @@
        {
            try
            {
                CanDriverAdapter.ResetAllBuses();
                _canBus = null;
                // 更新 UI
@@ -729,7 +1146,7 @@
                canStartBtn.IsEnabled = true;
                canStopBtn.IsEnabled = false;
                Log("[CAN] 所有总线已重置");
                Log("[CAN] 本地状态已重置(外部模拟器总线不受影响)");
            }
            catch (Exception ex)
            {