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 { /// /// 单设备调试工作区(自带独立调试会话与维护租约)。由 DeviceDebug 壳按 Tab 承载, /// 支持同时打开多个设备互不干扰。会话状态机沿用 DebugSessionManager(共享单例, /// 内存按 sessionId 缓存多会话),进入/退出维护只作用于本设备资源键。 /// [ObservableObject] public partial class DeviceDebugTab : UserControl { private static readonly ILog log = LogManager.GetLogger(typeof(DeviceDebugTab)); // 共享服务(由壳注入,生命周期属壳) private DeviceDebugModel _debugModel; private DebugSessionManager _sessionManager; private IDeviceManager _deviceManager; private IDeviceService _deviceService; private string _deviceConfigDbPath; [ObservableProperty] private DebugSession _currentSession; [ObservableProperty] private ObservableCollection _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; /// 本 Tab 关联的设备ID(供壳过滤已打开设备、关闭时定位会话)。 public string BoundDeviceId { get; private set; } = ""; public DeviceDebugTab() { DataContext = this; InitializeComponent(); } /// /// 注入共享服务并打开该设备的调试会话(ReadOnly)+ 加载面板。 /// 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(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}"); } } /// /// 关闭本 Tab 会话并释放维护租约(供退出维护按钮与壳关闭 Tab 共用)。 /// 经 sessionId 复用 token 释放,杜绝租约泄漏导致的“第二次进维护失败”。 /// public void CloseSessionSilently() { try { if (CurrentSession != null) { _sessionManager.CloseSession(CurrentSession.SessionId); CurrentSession = null; log.Info($"设备 {BoundDeviceId} 会话已关闭,维护租约已释放"); } // 重新打开 ReadOnly 会话,让用户可以再次进入维护模式 CurrentSession = _sessionManager.OpenSession(BoundDeviceId, "", "operator", CurrentEffectiveVersion); UpdateButtonStates(); LoadOperationLogs(); } 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; } } }