RobotStatusPushService.cs
11.2 KB
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
using MassTransit.Internals;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rcs.Api.Hubs;
using Rcs.Application.DTOs;
using Rcs.Application.Services;
using Rcs.Domain.Repositories;
using Rcs.Shared.Utils;
namespace Rcs.Api.BackgroundServices
{
/// <summary>
/// 机器人状态推送后台服务 - 定时推送机器人实时状态到前端
/// 推送策略:
/// 1. 订阅机器人状态的客户端只接收机器人状态
/// 2. 订阅库位信息的客户端只接收库位信息
/// @author zzy
/// </summary>
public class RobotStatusPushService : BackgroundService
{
private readonly ILogger<RobotStatusPushService> _logger;
private readonly IHubContext<HHRCSHub> _hubContext;
private readonly IServiceScopeFactory _scopeFactory;
private readonly TimeSpan _pushInterval = TimeSpan.FromMilliseconds(200);
private readonly TimeSpan _locationPushInterval = TimeSpan.FromSeconds(2);
public RobotStatusPushService(
ILogger<RobotStatusPushService> logger,
IHubContext<HHRCSHub> hubContext,
IServiceScopeFactory scopeFactory)
{
_logger = logger;
_hubContext = hubContext;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("[RobotStatusPush] 服务启动");
var robotStatusTask = RunRobotStatusLoopAsync(stoppingToken);
var storageLocationTask = RunStorageLocationLoopAsync(stoppingToken);
await Task.WhenAll(robotStatusTask, storageLocationTask);
}
private async Task RunRobotStatusLoopAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await PushRobotStatusAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "[RobotStatusPush] 机器人状态推送异常");
}
try
{
await Task.Delay(_pushInterval, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
}
}
private async Task RunStorageLocationLoopAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await PushStorageLocationsAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "[RobotStatusPush] 库位状态推送异常");
}
try
{
await Task.Delay(_locationPushInterval, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
}
}
/// <summary>
/// 推送机器人状态到订阅了机器人状态的客户端(纯缓存读取,不查数据库)
/// @author zzy
/// </summary>
private async Task PushRobotStatusAsync(CancellationToken cancellationToken)
{
using var scope = _scopeFactory.CreateScope();
var cacheService = scope.ServiceProvider.GetRequiredService<IRobotCacheService>();
var mapCacheService = scope.ServiceProvider.GetRequiredService<IMapCacheService>();
var trafficControlService = scope.ServiceProvider.GetRequiredService<IUnifiedTrafficControlService>();
// 从缓存获取所有启用的机器人
var activeRobots = (await cacheService.GetAllActiveRobotCacheAsync())
.Where(r => r.Location?.MapId.HasValue == true)
.ToList();
if (activeRobots.Count == 0)
{
return;
}
// 预加载 MapId -> MapCode 映射,用于按当前地图筛选锁资源
var mapCodeById = new Dictionary<Guid, string>();
var mapIds = activeRobots
.Select(r => r.Location!.MapId!.Value)
.Distinct()
.ToList();
foreach (var mapId in mapIds)
{
var mapData = await mapCacheService.GetMapAsync(mapId);
if (!string.IsNullOrWhiteSpace(mapData?.MapCode))
{
mapCodeById[mapId] = mapData.MapCode;
}
}
// 预加载每台机器人在当前地图中的节点锁/边锁
var lockCodesByRobotId = new Dictionary<Guid, (List<string> NodeCodes, List<string> EdgeCodes)>();
foreach (var robot in activeRobots)
{
if (!Guid.TryParse(robot.Basic.RobotId, out var robotId))
{
continue;
}
var mapId = robot.Location!.MapId!.Value;
if (!mapCodeById.TryGetValue(mapId, out var mapCode))
{
continue;
}
var (nodeKeys, edgeKeys) = await trafficControlService.GetRobotLockedResourcesAsync(robotId);
lockCodesByRobotId[robotId] = (
ExtractLockCodesByMap(nodeKeys, mapCode),
ExtractLockCodesByMap(edgeKeys, mapCode)
);
}
// 按地图分组
var robotsByMap = activeRobots
.GroupBy(r => r.Location!.MapId!.Value);
foreach (var mapGroup in robotsByMap)
{
var mapId = mapGroup.Key.ToString();
var statusList = mapGroup
.OrderBy(r => r.Basic.RobotCode)
.Select(r =>
{
var lockedNodeCodes = new List<string>();
var lockedEdgeCodes = new List<string>();
if (Guid.TryParse(r.Basic.RobotId, out var robotId) &&
lockCodesByRobotId.TryGetValue(robotId, out var lockCodes))
{
lockedNodeCodes = lockCodes.NodeCodes;
lockedEdgeCodes = lockCodes.EdgeCodes;
}
return new RobotRealtimeStatusDto
{
RobotId = r.Basic.RobotId,
RobotType = r.Basic.RobotType,
RobotCode = r.Basic.RobotCode,
RobotName = r.Basic.RobotName,
X = r.Location?.X,
Y = r.Location?.Y,
Theta = AngleConverter.ToCycleDegrees(r.Location?.Theta),
Status = r.Status != null ? (int)r.Status.Status : 1,
Online = r.Status != null ? (int)r.Status.Online : 2,
BatteryLevel = r.Status?.BatteryLevel,
Driving = r.Status?.Driving ?? false,
Paused = r.Status?.Paused ?? false,
Charging = r.Status?.Charging ?? false,
Errors = r.Status?.Errors,
Path = r.Location?.Path,
CurNode = r.Location?.NodeId?.ToString(),
LockedNodeCodes = lockedNodeCodes,
LockedEdgeCodes = lockedEdgeCodes
};
})
.ToList();
// 向订阅了该地图机器人状态的客户端推送(地图隔离)
await _hubContext.Clients.Group($"{mapId}_robot_status")
.SendAsync("RobotStatusUpdate", statusList, cancellationToken);
}
}
/// <summary>
/// 推送库位状态到订阅了库位信息的客户端(按地图分组,降频推送)
/// @author zzy
/// </summary>
private async Task PushStorageLocationsAsync(CancellationToken cancellationToken)
{
using var scope = _scopeFactory.CreateScope();
var mapRepo = scope.ServiceProvider.GetRequiredService<IMapRepository>();
var nodeRepo = scope.ServiceProvider.GetRequiredService<IMapNodeRepository>();
// 获取所有启用的地图
var maps = await mapRepo.GetAllAsync(cancellationToken);
foreach (var map in maps.Where(m => m.Active))
{
var nodes = await nodeRepo.GetLocationsByMapIdAsync(map.MapId, cancellationToken);
// 映射到 DTO 避免循环引用
var areasDto = nodes.Select(a => new
{
StorageLocations = a.StorageLocations.Select(loc => new
{
locationId = loc.LocationId.ToString(),
locationCode = loc.LocationCode,
locationName = loc.LocationName,
layerNumber = loc.LayerNumber,
status = loc.Status,
mapNodeId = loc.MapNodeId,
isActive = loc.IsActive
}).ToList(),
NodeId = a.NodeId,
NodeCode = a.NodeCode,
NodeName = a.NodeName,
}).ToList();
// 向订阅了该地图库位信息的客户端推送(地图隔离)
await _hubContext.Clients.Group($"{map.MapId}_storage_location")
.SendAsync("StorageLocationsUpdate", areasDto, cancellationToken);
}
}
private static List<string> ExtractLockCodesByMap(IEnumerable<string> lockKeys, string mapCode)
{
if (string.IsNullOrWhiteSpace(mapCode))
{
return new List<string>();
}
var marker = $":{mapCode}:";
return lockKeys
.Select(key =>
{
var index = key.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (index < 0)
{
return null;
}
var codeStartIndex = index + marker.Length;
return codeStartIndex >= key.Length ? null : key[codeStartIndex..];
})
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.ToList();
}
}
}