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