| | |
| | | using System.Collections.Concurrent; |
| | | using System.Net.Sockets; |
| | | using WIDESEAWCS_Core.LogHelper; |
| | | using WIDESEAWCS_QuartzJob; |
| | | using WIDESEAWCS_Tasks.SocketServer; |
| | | |
| | | namespace WIDESEAWCS_Tasks |
| | | { |
| | | /// <summary> |
| | | /// 机械手客户端连接管理器 - 负责TCP客户端连接管理和事件订阅 |
| | | /// 机械手客户端连接管理器 - 负责 TCP 客户端连接管理和事件订阅 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 核心职责: |
| | | /// 1. 维护与机械手设备的 TCP 连接状态 |
| | | /// 2. 确保每个客户端只启动一次消息处理循环 |
| | | /// 3. 管理客户端连接/断开的生命周期事件 |
| | | /// 4. 提供发送消息到客户端的接口 |
| | | /// </remarks> |
| | | public class RobotClientManager |
| | | { |
| | | /// <summary> |
| | | /// TCP Socket 服务器实例,用于管理所有客户端连接 |
| | | /// </summary> |
| | | private readonly TcpSocketServer _tcpSocket; |
| | | |
| | | /// <summary> |
| | | /// 机械手状态管理器,用于读写设备状态 |
| | | /// </summary> |
| | | private readonly RobotStateManager _stateManager; |
| | | |
| | | // 跟踪已经启动 HandleClientAsync 的客户端 |
| | | /// <summary> |
| | | /// 跟踪已启动消息处理的客户端,避免重复启动 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// Key: 客户端 IP 地址 |
| | | /// Value: 是否已启动(true 表示已启动) |
| | | /// 使用 ConcurrentDictionary 保证线程安全 |
| | | /// </remarks> |
| | | private static readonly ConcurrentDictionary<string, bool> _handleClientStarted = new(); |
| | | |
| | | /// <summary> |
| | | /// 事件订阅标志,确保 RobotReceived 事件只订阅一次 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 使用原子操作 Interlocked.CompareExchange 保证全局只订阅一次 |
| | | /// </remarks> |
| | | private static int _eventSubscribedFlag; |
| | | |
| | | /// <summary> |
| | | /// 客户端断开连接时触发的事件 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 事件参数包含断开连接的机械手状态信息 |
| | | /// </remarks> |
| | | public event EventHandler<RobotSocketState>? OnClientDisconnected; |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | | /// </summary> |
| | | /// <param name="tcpSocket">TCP Socket 服务器实例</param> |
| | | /// <param name="stateManager">状态管理器实例</param> |
| | | public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager) |
| | | { |
| | | _tcpSocket = tcpSocket; |
| | |
| | | /// <summary> |
| | | /// 确保客户端已连接并订阅消息事件 |
| | | /// </summary> |
| | | /// <param name="ipAddress">设备IP地址</param> |
| | | /// <remarks> |
| | | /// 这是 RobotJob Execute 方法中的核心检查逻辑: |
| | | /// 1. 验证客户端是否在线 |
| | | /// 2. 订阅断开事件(全局只执行一次) |
| | | /// 3. 确保消息处理循环已启动 |
| | | /// 4. 防止重复启动 HandleClientAsync |
| | | /// </remarks> |
| | | /// <param name="ipAddress">设备 IP 地址</param> |
| | | /// <param name="robotCrane">机器人设备信息</param> |
| | | /// <returns>客户端是否可用(已连接且消息处理已启动)</returns> |
| | | public bool EnsureClientSubscribed(string ipAddress, RobotCraneDevice robotCrane) |
| | | { |
| | | // 检查是否有该客户端连接 |
| | | // 从 TCP 服务器获取所有已连接客户端的 ID 列表 |
| | | var clientIds = _tcpSocket.GetClientIds(); |
| | | |
| | | // 检查该 IP 地址的客户端是否已连接 |
| | | bool isClientConnected = clientIds.Contains(ipAddress); |
| | | |
| | | // 如果客户端未连接 |
| | | if (!isClientConnected) |
| | | { |
| | | // 客户端未连接,清理 HandleClientAsync 状态 |
| | | // 清理该客户端的 HandleClientAsync 启动标志 |
| | | // 以便下次重连时可以重新启动处理 |
| | | _handleClientStarted.TryRemove(ipAddress, out _); |
| | | return false; |
| | | } |
| | | |
| | | // 订阅一次 robot 事件(全局一次)- message事件由RobotJob订阅 |
| | | // 订阅一次 RobotReceived 事件(全局只订阅一次) |
| | | // 使用 Interlocked.CompareExchange 实现原子操作,确保线程安全 |
| | | if (System.Threading.Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0) |
| | | { |
| | | // 绑定客户端断开连接的事件处理 |
| | | _tcpSocket.RobotReceived += OnRobotReceived; |
| | | Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 机器人TCP断开事件已订阅"); |
| | | // 记录日志(注意:日志内容为"客户端已断开连接",可能是遗留的占位文本) |
| | | QuartzLogger.Warn($"客户端已断开连接", robotCrane.DeviceName); |
| | | } |
| | | |
| | | // 获取TcpClient |
| | | // 从 TCP 服务器的客户端字典中获取 TcpClient 对象 |
| | | TcpClient? tcpClient = null; |
| | | _tcpSocket._clients.TryGetValue(ipAddress, out tcpClient); |
| | | |
| | | // 如果获取失败(虽然 isClientConnected 为 true,但可能存在字典不同步的情况) |
| | | if (tcpClient == null) |
| | | { |
| | | // isClientConnected为true但无法获取tcpClient,列表可能不同步 |
| | | // 移除启动标志,返回 false 表示客户端不可用 |
| | | _handleClientStarted.TryRemove(ipAddress, out _); |
| | | return false; |
| | | } |
| | | |
| | | // 检查是否已经为这个客户端启动过 HandleClientAsync |
| | | // 检查是否已经为这个客户端启动过消息处理循环 |
| | | bool alreadyStarted = _handleClientStarted.TryGetValue(ipAddress, out _); |
| | | |
| | | // 如果尚未启动,则启动消息处理循环 |
| | | if (!alreadyStarted) |
| | | { |
| | | Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 启动客户端消息处理: {ipAddress}"); |
| | | // 记录日志 |
| | | QuartzLogger.Info($"启动客户端消息处理", robotCrane.DeviceName); |
| | | |
| | | // 重新获取最新的 state 对象 |
| | | // 获取最新的状态对象 |
| | | var latestStateForSubscribe = _stateManager.GetState(ipAddress); |
| | | if (latestStateForSubscribe != null) |
| | | { |
| | | // 标记为已启动 |
| | | // 标记为已启动,防止重复启动 |
| | | _handleClientStarted[ipAddress] = true; |
| | | |
| | | // 异步启动客户端消息处理循环 |
| | | // 使用 TaskContinuationOptions.OnlyOnFaulted 捕获异常情况 |
| | | _ = _tcpSocket.HandleClientAsync(tcpClient, robotCrane.IPAddress, _tcpSocket._cts.Token, latestStateForSubscribe) |
| | | .ContinueWith(t => |
| | | { |
| | | // 如果处理出现异常 |
| | | if (t.IsFaulted) |
| | | { |
| | | // 记录错误日志 |
| | | QuartzLogger.Info($"监听客户端消息事件异常", robotCrane.DeviceName); |
| | | Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] HandleClientAsync error: {t.Exception?.GetBaseException().Message}"); |
| | | // 发生错误时,移除启动标志,允许下次重试 |
| | | _handleClientStarted.TryRemove(ipAddress, out _); |
| | | } |
| | | }, TaskContinuationOptions.OnlyOnFaulted); |
| | | |
| | | // 更新 IsEventSubscribed 状态 |
| | | // 安全更新状态,标记为已订阅消息事件 |
| | | _stateManager.TryUpdateStateSafely(ipAddress, s => |
| | | { |
| | | s.IsEventSubscribed = true; |
| | |
| | | } |
| | | } |
| | | |
| | | // 返回 true 表示客户端可用 |
| | | return true; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 事件:客户端断开连接时触发 |
| | | /// 事件处理:客户端断开连接时调用 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 触发时机:当 TCP 服务器检测到客户端断开连接时 |
| | | /// 处理逻辑: |
| | | /// 1. 清理 HandleClientAsync 启动标志 |
| | | /// 2. 重置设备状态(取消订阅、清除动作和状态) |
| | | /// 3. 触发 OnClientDisconnected 事件通知上层 |
| | | /// </remarks> |
| | | /// <param name="clientId">断开连接的客户端 IP 地址</param> |
| | | /// <returns>固定返回 null,因为是事件处理器而非真正的消息处理器</returns> |
| | | private Task<string?> OnRobotReceived(string clientId) |
| | | { |
| | | // 客户端断开连接,清理 HandleClientAsync 启动标志 |
| | | // 移除该客户端的 HandleClientAsync 启动标志 |
| | | _handleClientStarted.TryRemove(clientId, out _); |
| | | |
| | | // 重置该客户端的状态信息 |
| | | _stateManager.TryUpdateStateSafely(clientId, state => |
| | | { |
| | | state.IsEventSubscribed = false; |
| | | state.CurrentAction = ""; |
| | | state.OperStatus = ""; |
| | | state.RobotArmObject = 0; |
| | | state.RobotControlMode = 0; |
| | | state.RobotRunMode = 0; |
| | | state.IsEventSubscribed = false; // 取消订阅标志 |
| | | state.CurrentAction = ""; // 清除当前动作 |
| | | state.OperStatus = ""; // 清除运行状态 |
| | | state.RobotArmObject = 0; // 重置手臂对象状态 |
| | | state.RobotControlMode = 0; // 重置控制模式 |
| | | state.RobotRunMode = 0; // 重置运行模式 |
| | | return state; |
| | | }); |
| | | |
| | | // 触发断开连接事件 |
| | | // 触发客户端断开连接事件,通知上层(如 RobotJob) |
| | | // 使用空的状态对象作为后备(如果获取不到) |
| | | OnClientDisconnected?.Invoke(this, _stateManager.GetState(clientId) ?? new RobotSocketState { IPAddress = clientId }); |
| | | |
| | | // 返回 null,因为这是事件处理而非真正的消息路由 |
| | | return Task.FromResult<string?>(null); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查客户端是否已连接 |
| | | /// </summary> |
| | | /// <param name="ipAddress">设备 IP 地址</param> |
| | | /// <returns>如果已连接则返回 true</returns> |
| | | public bool IsClientConnected(string ipAddress) |
| | | { |
| | | // 获取所有已连接客户端的 ID 列表 |
| | | var clientIds = _tcpSocket.GetClientIds(); |
| | | // 检查列表中是否包含指定的 IP 地址 |
| | | return clientIds.Contains(ipAddress); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 发送消息到客户端 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 封装 TcpSocketServer 的发送方法,提供更简洁的接口给业务层 |
| | | /// </remarks> |
| | | /// <param name="ipAddress">目标客户端 IP 地址</param> |
| | | /// <param name="message">要发送的消息内容</param> |
| | | /// <returns>发送是否成功</returns> |
| | | public async Task<bool> SendToClientAsync(string ipAddress, string message) |
| | | { |
| | | return await _tcpSocket.SendToClientAsync(ipAddress, message); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取TcpSocketServer引用(用于RobotJob直接访问) |
| | | /// 获取 TcpSocketServer 引用 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// RobotJob 可能需要直接访问 TcpSocketServer 进行配置 |
| | | /// 此属性提供只读访问 |
| | | /// </remarks> |
| | | public TcpSocketServer TcpSocket => _tcpSocket; |
| | | } |
| | | } |