wanshenmean
3 天以前 5e851678cc02257bbbd179446de36082430ca5bc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
using System.Collections.Concurrent;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.SocketServer;
 
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// 机械手客户端连接管理器 - 负责 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;
 
        /// <summary>
        /// 日志记录器
        /// </summary>
        private readonly ILogger _logger;
 
        /// <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>
        /// <param name="logger">日志记录器</param>
        public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager, ILogger logger)
        {
            _tcpSocket = tcpSocket;
            _stateManager = stateManager;
            _logger = logger;
        }
 
        /// <summary>
        /// 确保客户端已连接并订阅消息事件
        /// </summary>
        /// <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 启动标志
                // 以便下次重连时可以重新启动处理
                _handleClientStarted.TryRemove(ipAddress, out _);
                _logger.LogDebug("客户端未连接,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"客户端未连接,IP: {ipAddress}", robotCrane.DeviceName);
                return false;
            }
 
            // 订阅一次 RobotReceived 事件(全局只订阅一次)
            // 使用 Interlocked.CompareExchange 实现原子操作,确保线程安全
            if (System.Threading.Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0)
            {
                // 绑定客户端断开连接的事件处理
                _tcpSocket.RobotReceived += OnRobotReceived;
                // 记录日志:事件订阅成功
                _logger.LogInformation("机械手TCP消息事件已订阅,设备: {DeviceName}", robotCrane.DeviceName);
                QuartzLogger.Info($"机械手TCP消息事件已订阅", robotCrane.DeviceName);
            }
 
            // 从 TCP 服务器的客户端字典中获取 TcpClient 对象
            TcpClient? tcpClient = null;
            _tcpSocket._clients.TryGetValue(ipAddress, out tcpClient);
 
            // 如果获取失败(虽然 isClientConnected 为 true,但可能存在字典不同步的情况)
            if (tcpClient == null)
            {
                // 移除启动标志,返回 false 表示客户端不可用
                _handleClientStarted.TryRemove(ipAddress, out _);
                _logger.LogWarning("获取TcpClient失败,IP: {IpAddress}", ipAddress);
                QuartzLogger.Warn($"获取TcpClient失败,IP: {ipAddress}", robotCrane.DeviceName);
                return false;
            }
 
            // 检查是否已经为这个客户端启动过消息处理循环
            bool alreadyStarted = _handleClientStarted.TryGetValue(ipAddress, out _);
 
            // 如果尚未启动,则启动消息处理循环
            if (!alreadyStarted)
            {
                // 记录日志:启动消息处理
                _logger.LogInformation("启动客户端消息处理,IP: {IpAddress}", ipAddress);
                QuartzLogger.Info($"启动客户端消息处理", robotCrane.DeviceName);
 
                // 获取最新的状态对象
                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)
                            {
                                // 记录错误日志
                                _logger.LogError(t.Exception, "监听客户端消息事件异常,IP: {IpAddress}", ipAddress);
                                QuartzLogger.Error($"监听客户端消息事件异常", robotCrane.DeviceName, t.Exception);
                                // 发生错误时,移除启动标志,允许下次重试
                                _handleClientStarted.TryRemove(ipAddress, out _);
                            }
                        }, TaskContinuationOptions.OnlyOnFaulted);
 
                    // 安全更新状态,标记为已订阅消息事件
                    _stateManager.TryUpdateStateSafely(ipAddress, s =>
                    {
                        s.IsEventSubscribed = true;
                        return s;
                    });
                }
            }
 
            // 返回 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 启动标志
            _handleClientStarted.TryRemove(clientId, out _);
 
            // 记录日志:客户端断开连接
            _logger.LogInformation("客户端断开连接,IP: {ClientId}", clientId);
            QuartzLogger.Info($"客户端断开连接", clientId);
 
            // 重置该客户端的状态信息
            _stateManager.TryUpdateStateSafely(clientId, state =>
            {
                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 引用
        /// </summary>
        /// <remarks>
        /// RobotJob 可能需要直接访问 TcpSocketServer 进行配置
        /// 此属性提供只读访问
        /// </remarks>
        public TcpSocketServer TcpSocket => _tcpSocket;
    }
}