From 559890c482f5973f06aafdfec9550fee5ba3c7f2 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期一, 09 十一月 2026 17:02:15 +0800
Subject: [PATCH] feat: CAN 模拟器 TCP 化 + 多设备并发调试 + 后台生命周期管理

---
 Plugin.DeviceDebug/DeviceDebugTab.xaml.cs |  458 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 458 insertions(+), 0 deletions(-)

diff --git a/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs b/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs
new file mode 100644
index 0000000..e868f3e
--- /dev/null
+++ b/Plugin.DeviceDebug/DeviceDebugTab.xaml.cs
@@ -0,0 +1,458 @@
+using Aging.Infrastructure.Contracts;
+using CommunityToolkit.Mvvm.ComponentModel;
+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.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace Plugin.DeviceDebug
+{
+    /// <summary>
+    /// 鍗曡澶囪皟璇曞伐浣滃尯锛堣嚜甯︾嫭绔嬭皟璇曚細璇濅笌缁存姢绉熺害锛夈�傜敱 DeviceDebug 澹虫寜 Tab 鎵胯浇锛�
+    /// 鏀寔鍚屾椂鎵撳紑澶氫釜璁惧浜掍笉骞叉壈銆備細璇濈姸鎬佹満娌跨敤 DebugSessionManager锛堝叡浜崟渚嬶紝
+    /// 鍐呭瓨鎸� sessionId 缂撳瓨澶氫細璇濓級锛岃繘鍏�/閫�鍑虹淮鎶ゅ彧浣滅敤浜庢湰璁惧璧勬簮閿��
+    /// </summary>
+    [ObservableObject]
+    public partial class DeviceDebugTab : UserControl
+    {
+        private static readonly ILog log = LogManager.GetLogger(typeof(DeviceDebugTab));
+
+        // 鍏变韩鏈嶅姟锛堢敱澹虫敞鍏ワ紝鐢熷懡鍛ㄦ湡灞炲3锛�
+        private DeviceDebugModel _debugModel;
+        private DebugSessionManager _sessionManager;
+        private IDeviceManager _deviceManager;
+        private IDeviceService _deviceService;
+        private string _deviceConfigDbPath;
+
+        [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;
+
+        /// <summary>鏈� Tab 鍏宠仈鐨勮澶嘔D锛堜緵澹宠繃婊ゅ凡鎵撳紑璁惧銆佸叧闂椂瀹氫綅浼氳瘽锛夈��</summary>
+        public string BoundDeviceId { get; private set; } = "";
+
+        public DeviceDebugTab()
+        {
+            DataContext = this;
+            InitializeComponent();
+        }
+
+        /// <summary>
+        /// 娉ㄥ叆鍏变韩鏈嶅姟骞舵墦寮�璇ヨ澶囩殑璋冭瘯浼氳瘽锛圧eadOnly锛�+ 鍔犺浇闈㈡澘銆�
+        /// </summary>
+        public void Initialize(DeviceDebugModel debugModel, DebugSessionManager sessionManager,
+            IDeviceManager deviceManager, IDeviceService deviceService, string deviceConfigDbPath,
+            string deviceId, string kind)
+        {
+            _debugModel = debugModel;
+            _sessionManager = sessionManager;
+            _deviceManager = deviceManager;
+            _deviceService = deviceService;
+            _deviceConfigDbPath = deviceConfigDbPath;
+            BoundDeviceId = deviceId;
+            OpenDevice(deviceId, kind);
+        }
+
+        private void OpenDevice(string deviceId, string kind)
+        {
+            try
+            {
+                LoadDeviceInfo(deviceId);
+                HasDevice = true;
+
+                CurrentSession = _sessionManager.OpenSession(deviceId, "", "operator", CurrentEffectiveVersion);
+
+                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($"璋冭瘯 Tab 宸叉墦寮�璁惧: {deviceId}, 绫诲瀷: {kind}");
+            }
+            catch (Exception ex)
+            {
+                log.Error("鎵撳紑璁惧璋冭瘯澶辫触", ex);
+                ToastHelper.Error($"鎵撳紑璁惧澶辫触: {ex.Message}");
+            }
+        }
+
+        private void LoadDeviceInfo(string deviceId)
+        {
+            try
+            {
+                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() ?? "";
+                }
+            }
+            catch (Exception ex)
+            {
+                log.Error($"鍔犺浇璁惧淇℃伅澶辫触: {deviceId}", ex);
+            }
+        }
+
+        private void LoadPanel(string deviceKind)
+        {
+            try
+            {
+                UIElement panel = null;
+                switch (deviceKind)
+                {
+                    case "PowerSupply":
+                        var powerPanel = new PowerSupplyDebugPanel
+                        {
+                            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
+                        };
+                        // 鍏变韩/Broker 璁惧澶嶇敤 DeviceManager 宸叉敞鍐岄┍鍔紝绂佹鑷缓鐗╃悊浼氳瘽
+                        try
+                        {
+                            var registeredDriver = _deviceManager?.GetDriver(CurrentDeviceId);
+                            if (registeredDriver != null)
+                            {
+                                canPanel.ExistingDriver = registeredDriver;
+                                log.Info($"璁惧 {CurrentDeviceId} 璋冭瘯闈㈡澘澶嶇敤宸叉敞鍐岄┍鍔�: {registeredDriver.GetType().Name}");
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            log.Warn($"鑾峰彇宸叉敞鍐岄┍鍔ㄥけ璐ワ紝闈㈡澘闄嶇骇鏈湴鐩撮┍: {ex.Message}");
+                        }
+                        canPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive;
+                        canPanel.OnCommandExecute += Panel_OnCommandExecute;
+                        panel = canPanel;
+                        break;
+                    case "PlcDevice":
+                        var plcPanel = new PlcDebugPanel
+                        {
+                            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}");
+            }
+        }
+
+        private void Panel_OnCommandExecute(object sender, DebugCommand command)
+        {
+            try
+            {
+                if (CurrentSession == null)
+                {
+                    ToastHelper.Warning("璇峰厛閫夋嫨璁惧");
+                    return;
+                }
+                if (!_sessionManager.ValidateCommand(CurrentSession.SessionId, command))
+                {
+                    ToastHelper.Warning("褰撳墠浼氳瘽鐘舵�佷笉鍏佽鎵ц姝ゆ搷浣滐紝璇峰厛杩涘叆缁存姢妯″紡");
+                    return;
+                }
+                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);
+            }
+        }
+
+        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);
+            }
+        }
+
+        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} 宸插彂閫侊紙闄嶇骇妯″紡锛�");
+        }
+
+        private string BuildDeviceCommand(DebugCommand command)
+        {
+            if (command is PowerSetCommand psc)
+                return $"VOLT {psc.Voltage};CURR {psc.CurrentLimit};OUTP {(psc.Output ? "ON" : "OFF")}";
+            if (command is PowerMeasureCommand)
+                return "MEAS:ALL?";
+            if (command is CanSendCommand csc)
+                return $"CAN:SEND {csc.Frame}";
+            if (command is PlcWriteCommand pwc)
+                return $"WRITE:{pwc.IoAddress}:{(pwc.Value ? "TRUE" : "FALSE")}";
+            return command.CommandType;
+        }
+
+        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 "{}"; }
+        }
+
+        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); }
+        }
+
+        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;
+        }
+
+        private void EnterMaintenanceBtn_Click(object sender, RoutedEventArgs e)
+        {
+            try
+            {
+                if (CurrentSession == null) return;
+                if (_sessionManager.RequestMaintenance(CurrentSession.SessionId))
+                {
+                    _sessionManager.ActivateMaintenance(CurrentSession.SessionId);
+                    UpdateButtonStates();
+                    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)
+                    CloseSessionSilently();
+            }
+            catch (Exception ex)
+            {
+                log.Error("閫�鍑虹淮鎶ゆā寮忓け璐�", ex);
+                ToastHelper.Error($"閫�鍑虹淮鎶ゆā寮忓け璐�: {ex.Message}");
+            }
+        }
+
+        /// <summary>
+        /// 鍏抽棴鏈� Tab 浼氳瘽骞堕噴鏀剧淮鎶ょ绾︼紙渚涢��鍑虹淮鎶ゆ寜閽笌澹冲叧闂� Tab 鍏辩敤锛夈��
+        /// 缁� sessionId 澶嶇敤 token 閲婃斁锛屾潨缁濈绾︽硠婕忓鑷寸殑鈥滅浜屾杩涚淮鎶ゅけ璐モ�濄��
+        /// </summary>
+        public void CloseSessionSilently()
+        {
+            try
+            {
+                if (CurrentSession != null)
+                {
+                    _sessionManager.CloseSession(CurrentSession.SessionId);
+                    CurrentSession = null;
+                    log.Info($"璁惧 {BoundDeviceId} 浼氳瘽宸插叧闂紝缁存姢绉熺害宸查噴鏀�");
+                }
+            }
+            catch (Exception ex)
+            {
+                log.Warn($"鍏抽棴浼氳瘽寮傚父: {ex.Message}");
+            }
+        }
+
+        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;
+        }
+
+        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;
+        }
+    }
+}

--
Gitblit v1.9.1