wenxiangchao
4 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
Plugin.DeviceDebug/DeviceDebug.xaml.cs
@@ -4,58 +4,41 @@
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.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Plugin.DeviceDebug
{
    /// <summary>
    /// DeviceDebug.xaml 的交互逻辑
    /// 硬件调试插件(壳):装配并持有共享基础设施(会话管理器/设备服务/配置库路径),
    /// 以 TabControl 承载多个 <see cref="DeviceDebugTab"/>,支持同时调试多个设备,
    /// 每个 Tab 拥有独立会话与维护租约。设备下拉仅列出尚未打开的设备。
    /// </summary>
    [ObservableObject]
    public partial class DeviceDebug : SimpleMaskControl
    {
        private static readonly ILog _log = LogManager.GetLogger(typeof(DeviceDebug));
        private static readonly ILog log = LogManager.GetLogger(typeof(DeviceDebug));
        private DeviceDebugModel _debugModel;
        private DebugSessionManager _sessionManager;
        // 基础设施层服务
        private IDeviceManager? _deviceManager;
        private IDeviceService? _deviceService;
        private IResourceManager? _resourceManager;
        private IDeviceManager _deviceManager;
        private IDeviceService _deviceService;
        private IResourceManager _resourceManager;
        // 设备配置数据库路径(与 DeviceConfig 插件共享)
        private string _deviceConfigDbPath;
        // 全量设备("id [kind]")与已打开标签映射(deviceId → TabItem)
        private readonly List<string> _allDevices = new();
        private readonly Dictionary<string, TabItem> _openTabs = new(StringComparer.OrdinalIgnoreCase);
        [ObservableProperty] private ObservableCollection<string> _deviceList = new();
        [ObservableProperty] private string _selectedDevice;
        [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;
        // 缓存当前选中的设备类型
        private string _currentDeviceKindStr;
        public DeviceDebug()
        {
@@ -63,13 +46,10 @@
            InitializeComponent();
        }
        /// <summary>
        /// 强制刷新设备列表(供集成测试预置设备后调用)
        /// </summary>
        public void ForceRefreshForTest()
        {
            try { LoadDeviceList(); }
            catch (Exception ex) { _log.Error("ForceRefreshForTest Error.", ex); }
            catch (Exception ex) { log.Error("ForceRefreshForTest Error.", ex); }
        }
        public override void InitFinish()
@@ -77,21 +57,16 @@
            base.InitFinish();
            try
            {
                // 初始化调试数据库
                _debugModel = new DeviceDebugModel();
                string dbPath = Path.Combine(BaseFolder, "debug.db");
                _debugModel.InitDatabase(dbPath);
                // 从 UtilContainer 获取基础设施服务
                var container = new UtilContainer();
                _deviceManager = container.GetUtil<IDeviceManager>("DeviceManager");
                _deviceService = container.GetUtil<IDeviceService>("DeviceService");
                _resourceManager = container.GetUtil<IResourceManager>("ResourceManager");
                // 创建会话管理器(传入 ResourceManager)
                _sessionManager = new DebugSessionManager(_debugModel, _resourceManager);
                // 设备配置数据库路径(与 DeviceConfig 共享)
                _deviceConfigDbPath = Path.Combine(BaseFolder, "data.db");
                log.Info("硬件调试模块初始化完成");
@@ -103,9 +78,14 @@
            }
        }
        /// <summary>
        /// 加载设备列表
        /// </summary>
        private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e)
        {
            LoadDeviceList();
            Loaded -= SimpleMaskControl_Loaded;
        }
        private void RfhBtn_Click(object sender, RoutedEventArgs e) => LoadDeviceList();
        private void LoadDeviceList()
        {
            try
@@ -120,16 +100,12 @@
                var sqliteHelper = new SqliteHelper(_deviceConfigDbPath);
                var dt = sqliteHelper.GetDataTable("SELECT DeviceId, Kind FROM DeviceDefinition ORDER BY DeviceId");
                var list = new ObservableCollection<string>();
                _allDevices.Clear();
                foreach (System.Data.DataRow row in dt.Rows)
                {
                    string deviceId = row["DeviceId"].ToString();
                    string kind = row["Kind"].ToString();
                    list.Add($"{deviceId} [{kind}]");
                }
                    _allDevices.Add($"{row["DeviceId"]} [{row["Kind"]}]");
                DeviceList = list;
                log.Info($"加载设备列表完成,共 {list.Count} 个设备");
                RefreshAvailableList();
                log.Info($"加载设备列表完成,共 {_allDevices.Count} 个设备");
            }
            catch (Exception ex)
            {
@@ -138,20 +114,23 @@
            }
        }
        /// <summary>
        /// 解析设备选择字符串,获取 DeviceId
        /// </summary>
        private string ParseDeviceId(string selection)
        /// <summary>下拉仅列出未打开的设备(一会一设备)。</summary>
        private void RefreshAvailableList()
        {
            var avail = _allDevices
                .Where(d => !_openTabs.ContainsKey(ParseDeviceId(d)))
                .ToList();
            DeviceList = new ObservableCollection<string>(avail);
        }
        private static string ParseDeviceId(string selection)
        {
            if (string.IsNullOrEmpty(selection)) return string.Empty;
            int idx = selection.IndexOf(" [");
            return idx > 0 ? selection.Substring(0, idx) : selection;
        }
        /// <summary>
        /// 解析设备选择字符串,获取 Kind
        /// </summary>
        private string ParseDeviceKind(string selection)
        private static string ParseDeviceKind(string selection)
        {
            if (string.IsNullOrEmpty(selection)) return string.Empty;
            int start = selection.IndexOf(" [");
@@ -161,508 +140,64 @@
            return string.Empty;
        }
        /// <summary>
        /// 加载设备详细信息
        /// </summary>
        private void LoadDeviceInfo(string deviceId)
        {
            try
            {
                if (!File.Exists(_deviceConfigDbPath)) return;
                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() ?? "";
                    // 缓存设备类型
                    _currentDeviceKindStr = CurrentDeviceKind;
                }
            }
            catch (Exception ex)
            {
                log.Error($"加载设备信息失败: {deviceId}", ex);
            }
        }
        /// <summary>
        /// 选择设备时:打开会话(ReadOnly),加载对应面板
        /// </summary>
        private void OnDeviceSelected()
        private void AddDeviceBtn_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (string.IsNullOrEmpty(SelectedDevice))
                {
                    HasDevice = false;
                    panelContainer.Content = new TextBlock
                    {
                        Text = "请选择设备",
                        HorizontalAlignment = HorizontalAlignment.Center,
                        VerticalAlignment = VerticalAlignment.Center,
                        FontSize = 16,
                        Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999"))
                    };
                    ToastHelper.Warning("请先在设备下拉中选择设备");
                    return;
                }
                string deviceId = ParseDeviceId(SelectedDevice);
                string kind = ParseDeviceKind(SelectedDevice);
                // 加载设备详细信息
                LoadDeviceInfo(deviceId);
                HasDevice = true;
                // 打开会话(ReadOnly)
                CurrentSession = _sessionManager.OpenSession(deviceId, "", "operator", CurrentEffectiveVersion);
                // 通过 DeviceManager 连接设备
                if (_deviceManager != null)
                // 已打开则聚焦其 Tab(防御性:下拉本已过滤)
                if (_openTabs.TryGetValue(deviceId, out var existing))
                {
                    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($"已选择设备: {deviceId}, 类型: {kind}");
            }
            catch (Exception ex)
            {
                log.Error("选择设备失败", ex);
                ToastHelper.Error($"选择设备失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 根据设备类型加载对应的调试面板
        /// </summary>
        private void LoadPanel(string deviceKind)
        {
            try
            {
                UIElement panel = null;
                switch (deviceKind)
                {
                    case "PowerSupply":
                        var powerPanel = new PowerSupplyDebugPanel();
                        powerPanel.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
                        };
                        canPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive;
                        canPanel.OnCommandExecute += Panel_OnCommandExecute;
                        panel = canPanel;
                        break;
                    case "PlcDevice":
                        var plcPanel = new PlcDebugPanel();
                        plcPanel.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}");
            }
        }
        /// <summary>
        /// 面板命令执行事件处理
        /// </summary>
        private void Panel_OnCommandExecute(object sender, DebugCommand command)
        {
            try
            {
                if (CurrentSession == null)
                {
                    ToastHelper.Warning("请先选择设备");
                    return;
                }
                // 验证命令权限
                if (!_sessionManager.ValidateCommand(CurrentSession.SessionId, command))
                {
                    ToastHelper.Warning("当前会话状态不允许执行此操作,请先进入维护模式");
                    return;
                }
                // 通过 DeviceService 执行实际命令
                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);
            }
        }
        /// <summary>
        /// 通过 DeviceService 执行设备命令
        /// </summary>
        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);
            }
        }
        /// <summary>
        /// 降级模式:无 DeviceService 时的后备处理
        /// </summary>
        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} 已发送(降级模式)");
            log.Info($"[降级模式] 执行命令: {command.CommandType}, 设备: {command.DeviceId}");
        }
        /// <summary>
        /// 根据命令类型构建实际设备命令字符串
        /// </summary>
        private string BuildDeviceCommand(DebugCommand command)
        {
            if (command is PowerSetCommand psc)
            {
                return $"VOLT {psc.Voltage};CURR {psc.CurrentLimit};OUTP {(psc.Output ? "ON" : "OFF")}";
            }
            else if (command is PowerMeasureCommand)
            {
                return "MEAS:ALL?";
            }
            else if (command is CanSendCommand csc)
            {
                return $"CAN:SEND {csc.Frame}";
            }
            else if (command is PlcWriteCommand pwc)
            {
                return $"WRITE:{pwc.IoAddress}:{(pwc.Value ? "TRUE" : "FALSE")}";
            }
            return command.CommandType;
        }
        /// <summary>
        /// 序列化命令参数为JSON字符串
        /// </summary>
        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 "{}";
            }
        }
        /// <summary>
        /// 加载操作日志
        /// </summary>
        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);
            }
        }
        /// <summary>
        /// 更新按钮启用状态
        /// </summary>
        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;
        }
        #region 事件处理
        private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e)
        {
            LoadDeviceList();
            Loaded -= SimpleMaskControl_Loaded;
        }
        private void DeviceSelector_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            OnDeviceSelected();
        }
        private void RfhBtn_Click(object sender, RoutedEventArgs e)
        {
            LoadDeviceList();
        }
        private void EnterMaintenanceBtn_Click(object sender, RoutedEventArgs e)
        {
            try
            {
                if (CurrentSession == null) return;
                // 请求维护模式
                if (_sessionManager.RequestMaintenance(CurrentSession.SessionId))
                {
                    // 激活维护模式
                    _sessionManager.ActivateMaintenance(CurrentSession.SessionId);
                    UpdateButtonStates();
                    // 重新加载面板以更新IsMaintenanceMode
                    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)
                {
                    _sessionManager.CloseSession(CurrentSession.SessionId);
                    CurrentSession = null;
                    UpdateButtonStates();
                    // 清空面板
                    panelContainer.Content = new TextBlock
                    {
                        Text = "请选择设备",
                        HorizontalAlignment = HorizontalAlignment.Center,
                        VerticalAlignment = VerticalAlignment.Center,
                        FontSize = 16,
                        Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999"))
                    };
                    HasDevice = false;
                    debugTabs.SelectedItem = existing;
                    SelectedDevice = null;
                    ToastHelper.Success("已退出维护模式");
                    log.Info("已退出维护模式");
                    return;
                }
                var tabContent = new DeviceDebugTab();
                tabContent.Initialize(_debugModel, _sessionManager, _deviceManager, _deviceService,
                    _deviceConfigDbPath, deviceId, kind);
                var tab = new TabItem { Header = deviceId, Content = tabContent, Tag = deviceId };
                debugTabs.Items.Add(tab);
                debugTabs.SelectedItem = tab;
                _openTabs[deviceId] = tab;
                RefreshAvailableList();
                SelectedDevice = null;
                log.Info($"已加入调试设备: {deviceId}(当前 {debugTabs.Items.Count} 个标签)");
            }
            catch (Exception ex)
            {
                log.Error("退出维护模式失败", ex);
                ToastHelper.Error($"退出维护模式失败: {ex.Message}");
                log.Error("加入调试设备失败", ex);
                ToastHelper.Error($"加入调试失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 从 CAN Endpoint 解析总线名称(格式: 总线名:节点编号)
        /// </summary>
        private string ParseCanBusName(string endpoint)
        private void CloseTabBtn_Click(object sender, RoutedEventArgs e)
        {
            if (string.IsNullOrEmpty(endpoint)) return "CAN_BUS_1";
            int idx = endpoint.IndexOf(':');
            return idx > 0 ? endpoint.Substring(0, idx) : endpoint;
            try
            {
                if (debugTabs.SelectedItem is not TabItem tab) return;
                string deviceId = tab.Tag as string ?? "";
                if (tab.Content is DeviceDebugTab content)
                    content.CloseSessionSilently();   // 关闭即释放维护租约,杜绝泄漏
                debugTabs.Items.Remove(tab);
                if (!string.IsNullOrEmpty(deviceId)) _openTabs.Remove(deviceId);
                RefreshAvailableList();
                log.Info($"已关闭调试标签并释放会话: {deviceId}");
            }
            catch (Exception ex)
            {
                log.Error("关闭调试标签失败", ex);
                ToastHelper.Error($"关闭失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 从 CAN Endpoint 解析节点编号
        /// </summary>
        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;
        }
        #endregion
    }
}