| | 1 | | using Grpc.Core; |
| | 2 | | using LGDXRobotCloud.API.Configurations; |
| | 3 | | using LGDXRobotCloud.API.Services.Navigation; |
| | 4 | | using LGDXRobotCloud.Protos; |
| | 5 | | using LGDXRobotCloud.Utilities.Constants; |
| | 6 | | using Microsoft.AspNetCore.Authorization; |
| | 7 | | using Microsoft.Extensions.Options; |
| | 8 | | using Microsoft.IdentityModel.Tokens; |
| | 9 | | using static LGDXRobotCloud.Protos.RobotClientsService; |
| | 10 | | using System.IdentityModel.Tokens.Jwt; |
| | 11 | | using System.Security.Claims; |
| | 12 | | using System.Text; |
| | 13 | | using LGDXRobotCloud.Data.Models.Business.Navigation; |
| | 14 | | using LGDXRobotCloud.API.Services.Automation; |
| | 15 | | using StackExchange.Redis; |
| | 16 | | using static StackExchange.Redis.RedisChannel; |
| | 17 | | using LGDXRobotCloud.Utilities.Helpers; |
| | 18 | |
|
| | 19 | | namespace LGDXRobotCloud.API.Services; |
| | 20 | |
|
| | 21 | | [Authorize(AuthenticationSchemes = LgdxRobotCloudAuthenticationSchemes.RobotClientsJwtScheme)] |
| 7 | 22 | | public partial class RobotClientsService( |
| 7 | 23 | | IAutoTaskSchedulerService autoTaskSchedulerService, |
| 7 | 24 | | IConnectionMultiplexer redisConnection, |
| 7 | 25 | | ILogger<RobotClientsService> logger, |
| 7 | 26 | | IOnlineRobotsService OnlineRobotsService, |
| 7 | 27 | | IOptionsSnapshot<LgdxRobotCloudSecretConfiguration> lgdxRobotCloudSecretConfiguration, |
| 7 | 28 | | IRobotService robotService, |
| 7 | 29 | | ISlamService slamService |
| 7 | 30 | | ) : RobotClientsServiceBase |
| | 31 | | { |
| 7 | 32 | | private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN |
| 7 | 33 | | private readonly IConnectionMultiplexer _redisConnection = redisConnection ?? throw new ArgumentNullException(nameof(r |
| 7 | 34 | | private readonly IOnlineRobotsService _onlineRobotsService = OnlineRobotsService ?? throw new ArgumentNullException(na |
| 7 | 35 | | private readonly LgdxRobotCloudSecretConfiguration _lgdxRobotCloudSecretConfiguration = lgdxRobotCloudSecretConfigurat |
| 7 | 36 | | private readonly IRobotService _robotService = robotService ?? throw new ArgumentNullException(nameof(robotService)); |
| 7 | 37 | | private readonly ISlamService _slamService = slamService ?? throw new ArgumentNullException(nameof(slamService)); |
| | 38 | |
|
| | 39 | | [LoggerMessage(EventId = 0, Level = LogLevel.Error, Message = "gRPC RobotClientsService Exception: {Msg}")] |
| | 40 | | public partial void LogException(string msg); |
| | 41 | |
|
| 7 | 42 | | private bool pauseAutoTaskAssignmentEffective = false; |
| | 43 | |
|
| | 44 | | private static Guid GetRobotId(ServerCallContext context) |
| 0 | 45 | | { |
| 0 | 46 | | var robotClaim = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier); |
| 0 | 47 | | return Guid.Parse(robotClaim!.Value); |
| 0 | 48 | | } |
| | 49 | |
|
| | 50 | | [Authorize(AuthenticationSchemes = LgdxRobotCloudAuthenticationSchemes.RobotClientsCertificateScheme)] |
| | 51 | | public override async Task<RobotClientsGreetResponse> Greet(RobotClientsGreet request, ServerCallContext context) |
| 7 | 52 | | { |
| 7 | 53 | | var robotClaim = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier); |
| 7 | 54 | | if (robotClaim == null || !Guid.TryParse(robotClaim.Value, out var robotId)) |
| 2 | 55 | | return new RobotClientsGreetResponse |
| 2 | 56 | | { |
| 2 | 57 | | Status = RobotClientsResultStatus.Failed, |
| 2 | 58 | | AccessToken = string.Empty |
| 2 | 59 | | }; |
| | 60 | |
|
| 5 | 61 | | var robotIdGuid = robotId; |
| 5 | 62 | | var robot = await _robotService.GetRobotAsync(robotIdGuid); |
| 5 | 63 | | if (robot == null) |
| 1 | 64 | | return new RobotClientsGreetResponse |
| 1 | 65 | | { |
| 1 | 66 | | Status = RobotClientsResultStatus.Failed, |
| 1 | 67 | | AccessToken = string.Empty |
| 1 | 68 | | }; |
| | 69 | |
|
| | 70 | | // Compare System Info |
| 4 | 71 | | var incomingSystemInfo = new RobotSystemInfoBusinessModel |
| 4 | 72 | | { |
| 4 | 73 | | Id = 0, |
| 4 | 74 | | Cpu = request.SystemInfo.Cpu, |
| 4 | 75 | | IsLittleEndian = request.SystemInfo.IsLittleEndian, |
| 4 | 76 | | Motherboard = request.SystemInfo.Motherboard, |
| 4 | 77 | | MotherboardSerialNumber = request.SystemInfo.MotherboardSerialNumber, |
| 4 | 78 | | RamMiB = request.SystemInfo.RamMiB, |
| 4 | 79 | | Gpu = request.SystemInfo.Gpu, |
| 4 | 80 | | Os = request.SystemInfo.Os, |
| 4 | 81 | | Is32Bit = request.SystemInfo.Is32Bit, |
| 4 | 82 | | McuSerialNumber = request.SystemInfo.McuSerialNumber, |
| 4 | 83 | | }; |
| 4 | 84 | | var systemInfo = robot.RobotSystemInfo; |
| 4 | 85 | | if (systemInfo == null) |
| 1 | 86 | | { |
| | 87 | | // Create Robot System Info for the first time |
| 1 | 88 | | await _robotService.CreateRobotSystemInfoAsync(robotIdGuid, incomingSystemInfo.ToCreateBusinessModel()); |
| 1 | 89 | | } |
| | 90 | | else |
| 3 | 91 | | { |
| | 92 | | // Hardware Protection |
| 3 | 93 | | if (robot.IsProtectingHardwareSerialNumber && incomingSystemInfo.MotherboardSerialNumber != systemInfo.Motherboard |
| 1 | 94 | | { |
| 1 | 95 | | return new RobotClientsGreetResponse |
| 1 | 96 | | { |
| 1 | 97 | | Status = RobotClientsResultStatus.Failed, |
| 1 | 98 | | AccessToken = string.Empty |
| 1 | 99 | | }; |
| | 100 | | } |
| 2 | 101 | | if (robot.IsProtectingHardwareSerialNumber && incomingSystemInfo.McuSerialNumber != systemInfo.McuSerialNumber) |
| 1 | 102 | | { |
| 1 | 103 | | return new RobotClientsGreetResponse |
| 1 | 104 | | { |
| 1 | 105 | | Status = RobotClientsResultStatus.Failed, |
| 1 | 106 | | AccessToken = string.Empty |
| 1 | 107 | | }; |
| | 108 | | } |
| 1 | 109 | | await _robotService.UpdateRobotSystemInfoAsync(robotIdGuid, incomingSystemInfo.ToUpdateBusinessModel()); |
| 1 | 110 | | } |
| | 111 | |
|
| 2 | 112 | | var chassisInfo = await _robotService.GetRobotChassisInfoAsync(robotIdGuid); |
| | 113 | |
|
| | 114 | | // Generate Access Token |
| 2 | 115 | | var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_lgdxRobotCloudSecretConfiguration.RobotClientsJwt |
| 2 | 116 | | var credentials = new SigningCredentials(securityKey, _lgdxRobotCloudSecretConfiguration.RobotClientsJwtAlgorithm); |
| 2 | 117 | | var secToken = new JwtSecurityToken( |
| 2 | 118 | | _lgdxRobotCloudSecretConfiguration.RobotClientsJwtIssuer, |
| 2 | 119 | | _lgdxRobotCloudSecretConfiguration.RobotClientsJwtIssuer, |
| 2 | 120 | | [new Claim(ClaimTypes.NameIdentifier, robot.Id.ToString())], |
| 2 | 121 | | DateTime.UtcNow, |
| 2 | 122 | | DateTime.UtcNow.AddMinutes(_lgdxRobotCloudSecretConfiguration.RobotClientsJwtExpireMins), |
| 2 | 123 | | credentials); |
| 2 | 124 | | var token = new JwtSecurityTokenHandler().WriteToken(secToken); |
| | 125 | |
|
| 2 | 126 | | return new RobotClientsGreetResponse |
| 2 | 127 | | { |
| 2 | 128 | | Status = RobotClientsResultStatus.Success, |
| 2 | 129 | | AccessToken = token, |
| 2 | 130 | | ChassisInfo = new RobotClientsChassisInfo |
| 2 | 131 | | { |
| 2 | 132 | | RobotTypeId = chassisInfo!.RobotTypeId, |
| 2 | 133 | | ChassisLX = chassisInfo.ChassisLengthX, |
| 2 | 134 | | ChassisLY = chassisInfo.ChassisLengthY, |
| 2 | 135 | | ChassisWheelCount = chassisInfo.ChassisWheelCount, |
| 2 | 136 | | ChassisWheelRadius = chassisInfo.ChassisWheelRadius, |
| 2 | 137 | | BatteryCount = chassisInfo.BatteryCount, |
| 2 | 138 | | BatteryMaxVoltage = chassisInfo.BatteryMaxVoltage, |
| 2 | 139 | | BatteryMinVoltage = chassisInfo.BatteryMinVoltage, |
| 2 | 140 | | } |
| 2 | 141 | | }; |
| 7 | 142 | | } |
| | 143 | |
|
| | 144 | | /* |
| | 145 | | * Exchange |
| | 146 | | */ |
| | 147 | | public override async Task Exchange(IAsyncStreamReader<RobotClientsExchange> requestStream, IServerStreamWriter<RobotC |
| 0 | 148 | | { |
| 0 | 149 | | var robotId = GetRobotId(context); |
| 0 | 150 | | var clientToServer = ExchangeStreamClientToServerAsync(robotId, requestStream, context); |
| 0 | 151 | | var serverToClient = ExchangeStreamServerToClientAsync(robotId, responseStream, context); |
| 0 | 152 | | await Task.WhenAll(clientToServer, serverToClient); |
| 0 | 153 | | } |
| | 154 | |
|
| | 155 | | private async Task ExchangeStreamClientToServerAsync(Guid robotId, IAsyncStreamReader<RobotClientsExchange> requestStr |
| 0 | 156 | | { |
| | 157 | | try |
| 0 | 158 | | { |
| 0 | 159 | | await _onlineRobotsService.AddRobotAsync(robotId); |
| 0 | 160 | | await _autoTaskSchedulerService.RunSchedulerRobotNewJoinAsync(robotId); |
| 0 | 161 | | while (await requestStream.MoveNext(CancellationToken.None) && !context.CancellationToken.IsCancellationRequested) |
| 0 | 162 | | { |
| | 163 | | // 1. Process Data |
| 0 | 164 | | var request = requestStream.Current; |
| 0 | 165 | | await _onlineRobotsService.UpdateRobotDataAsync(robotId, request.RobotData); |
| 0 | 166 | | if (request.NextToken != null && request.NextToken.NextToken.Length > 0) |
| 0 | 167 | | { |
| 0 | 168 | | await _autoTaskSchedulerService.AutoTaskNextAsync(robotId, request.NextToken.TaskId, request.NextToken.NextTok |
| 0 | 169 | | } |
| 0 | 170 | | if (request.AbortToken != null && request.AbortToken.NextToken.Length > 0) |
| 0 | 171 | | { |
| 0 | 172 | | await _autoTaskSchedulerService.AutoTaskAbortAsync(robotId, request.AbortToken.TaskId, request.AbortToken.Next |
| 0 | 173 | | (Utilities.Enums.AutoTaskAbortReason)(int)request.AbortToken.AbortReason); |
| 0 | 174 | | } |
| | 175 | | // 2. Process Pause Auto Task Assignment |
| 0 | 176 | | if (pauseAutoTaskAssignmentEffective) |
| 0 | 177 | | { |
| 0 | 178 | | if (request.RobotData.RobotStatus != RobotClientsRobotStatus.Paused && |
| 0 | 179 | | !request.RobotData.PauseTaskAssignment) |
| 0 | 180 | | { |
| | 181 | | // Exit pause auto task assignment mode and request a task |
| 0 | 182 | | pauseAutoTaskAssignmentEffective = false; |
| 0 | 183 | | await _autoTaskSchedulerService.RunSchedulerRobotReadyAsync(robotId); |
| 0 | 184 | | } |
| 0 | 185 | | } |
| 0 | 186 | | else if (request.RobotData.RobotStatus == RobotClientsRobotStatus.Paused && |
| 0 | 187 | | request.RobotData.PauseTaskAssignment) |
| 0 | 188 | | { |
| | 189 | | // Enter pause auto task assignment mode |
| 0 | 190 | | pauseAutoTaskAssignmentEffective = true; |
| 0 | 191 | | } |
| 0 | 192 | | } |
| 0 | 193 | | } |
| 0 | 194 | | catch (Exception ex) |
| 0 | 195 | | { |
| 0 | 196 | | LogException(ex.Message); |
| 0 | 197 | | } |
| | 198 | | finally |
| 0 | 199 | | { |
| | 200 | | // The reading stream is completed, stop wirting task |
| 0 | 201 | | await _onlineRobotsService.RemoveRobotAsync(robotId); |
| 0 | 202 | | } |
| 0 | 203 | | } |
| | 204 | |
|
| | 205 | | private async Task ExchangeStreamServerToClientAsync(Guid robotId, IServerStreamWriter<RobotClientsResponse> responseS |
| 0 | 206 | | { |
| 0 | 207 | | await responseStream.WriteAsync(new RobotClientsResponse()); |
| | 208 | |
|
| 0 | 209 | | var subscriber = _redisConnection.GetSubscriber(); |
| 0 | 210 | | await subscriber.SubscribeAsync(new RedisChannel(RedisHelper.GetRobotExchangeQueue(robotId), PatternMode.Literal), ( |
| 0 | 211 | | { |
| 0 | 212 | | var response = SerialiserHelper.FromBase64<RobotClientsResponse>(value!); |
| 0 | 213 | | if (response != null) |
| 0 | 214 | | { |
| 0 | 215 | | responseStream.WriteAsync(response); |
| 0 | 216 | | } |
| 0 | 217 | | _autoTaskSchedulerService.ReleaseRobotAsync(robotId); |
| 0 | 218 | | }); |
| 0 | 219 | | context.CancellationToken.Register(() => |
| 0 | 220 | | { |
| 0 | 221 | | subscriber.UnsubscribeAllAsync(); |
| 0 | 222 | | }); |
| 0 | 223 | | } |
| | 224 | |
|
| | 225 | | /* |
| | 226 | | * SlamExchange |
| | 227 | | */ |
| | 228 | | public override async Task SlamExchange(IAsyncStreamReader<RobotClientsSlamExchange> requestStream, IServerStreamWrite |
| 0 | 229 | | { |
| 0 | 230 | | var robotId = GetRobotId(context); |
| 0 | 231 | | var realmId = await _robotService.GetRobotRealmIdAsync(robotId) ?? 0; |
| | 232 | |
|
| 0 | 233 | | var clientToServer = SlamExchangeClientToServerAsync(robotId, requestStream, context); |
| 0 | 234 | | var serverToClient = SlamExchangeServerToClientAsync(realmId, responseStream, context); |
| 0 | 235 | | await Task.WhenAll(clientToServer, serverToClient); |
| 0 | 236 | | } |
| | 237 | |
|
| | 238 | | private async Task SlamExchangeClientToServerAsync(Guid robotId, IAsyncStreamReader<RobotClientsSlamExchange> requestS |
| 0 | 239 | | { |
| | 240 | | try |
| 0 | 241 | | { |
| 0 | 242 | | if (await _slamService.StartSlamAsync(robotId)) |
| 0 | 243 | | { |
| | 244 | | // Only one robot can running SLAM at a time in a realm |
| | 245 | | // The second robot will be ternimated |
| 0 | 246 | | while (await requestStream.MoveNext(CancellationToken.None) && !context.CancellationToken.IsCancellationRequeste |
| 0 | 247 | | { |
| 0 | 248 | | var request = requestStream.Current; |
| 0 | 249 | | await _slamService.UpdateSlamDataAsync(robotId, request.Status, request.MapData); |
| 0 | 250 | | await _onlineRobotsService.UpdateRobotDataAsync(robotId, request.RobotData); |
| 0 | 251 | | } |
| 0 | 252 | | } |
| 0 | 253 | | } |
| 0 | 254 | | catch (Exception ex) |
| 0 | 255 | | { |
| 0 | 256 | | LogException(ex.Message); |
| 0 | 257 | | } |
| | 258 | | finally |
| 0 | 259 | | { |
| | 260 | | // The reading stream is completed, stop wirting task |
| 0 | 261 | | await _slamService.StopSlamAsync(robotId); |
| 0 | 262 | | } |
| 0 | 263 | | } |
| | 264 | |
|
| | 265 | | private async Task SlamExchangeServerToClientAsync(int realmId, IServerStreamWriter<RobotClientsSlamCommands> response |
| 0 | 266 | | { |
| 0 | 267 | | await responseStream.WriteAsync(new RobotClientsSlamCommands()); |
| | 268 | |
|
| 0 | 269 | | var subscriber = _redisConnection.GetSubscriber(); |
| 0 | 270 | | await subscriber.SubscribeAsync(new RedisChannel(RedisHelper.GetSlamExchangeQueue(realmId), PatternMode.Literal), (c |
| 0 | 271 | | { |
| 0 | 272 | | var response = SerialiserHelper.FromBase64<RobotClientsSlamCommands>(value!); |
| 0 | 273 | | if (response != null) |
| 0 | 274 | | { |
| 0 | 275 | | responseStream.WriteAsync(response); |
| 0 | 276 | | } |
| 0 | 277 | | }); |
| 0 | 278 | | context.CancellationToken.Register(() => |
| 0 | 279 | | { |
| 0 | 280 | | subscriber.UnsubscribeAllAsync(); |
| 0 | 281 | | }); |
| 0 | 282 | | } |
| | 283 | | } |