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));
|
|
// 共享服务(由壳注入,生命周期属壳)
|
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 关联的设备ID(供壳过滤已打开设备、关闭时定位会话)。</summary>
|
public string BoundDeviceId { get; private set; } = "";
|
|
public DeviceDebugTab()
|
{
|
DataContext = this;
|
InitializeComponent();
|
}
|
|
/// <summary>
|
/// 注入共享服务并打开该设备的调试会话(ReadOnly)+ 加载面板。
|
/// </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} 会话已关闭,维护租约已释放");
|
}
|
|
// 重新打开 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;
|
}
|
}
|
}
|