| | 1 | | using LGDXRobotCloud.API.Exceptions; |
| | 2 | | using LGDXRobotCloud.API.Services.Common; |
| | 3 | | using LGDXRobotCloud.API.Services.Navigation; |
| | 4 | | using LGDXRobotCloud.Data.DbContexts; |
| | 5 | | using LGDXRobotCloud.Data.Entities; |
| | 6 | | using LGDXRobotCloud.Data.Models.Business.Automation; |
| | 7 | | using LGDXRobotCloud.Data.Models.Business.Navigation; |
| | 8 | | using LGDXRobotCloud.Utilities.Enums; |
| | 9 | | using LGDXRobotCloud.Utilities.Helpers; |
| | 10 | | using MassTransit; |
| | 11 | | using Microsoft.EntityFrameworkCore; |
| | 12 | |
|
| | 13 | | namespace LGDXRobotCloud.API.Services.Automation; |
| | 14 | |
|
| | 15 | | public interface IAutoTaskService |
| | 16 | | { |
| | 17 | | Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? name, AutoTas |
| | 18 | | Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId); |
| | 19 | | Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel); |
| | 20 | | Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel); |
| | 21 | | Task<bool> DeleteAutoTaskAsync(int autoTaskId); |
| | 22 | |
|
| | 23 | | Task AbortAutoTaskAsync(int autoTaskId); |
| | 24 | | Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token); |
| | 25 | | } |
| | 26 | |
|
| 0 | 27 | | public class AutoTaskService( |
| 0 | 28 | | LgdxContext context, |
| 0 | 29 | | IAutoTaskSchedulerService autoTaskSchedulerService, |
| 0 | 30 | | IBus bus, |
| 0 | 31 | | IEventService eventService, |
| 0 | 32 | | IOnlineRobotsService onlineRobotsService |
| 0 | 33 | | ) : IAutoTaskService |
| | 34 | | { |
| 0 | 35 | | private readonly IOnlineRobotsService _onlineRobotsService = onlineRobotsService ?? throw new ArgumentNullException(na |
| 0 | 36 | | private readonly LgdxContext _context = context ?? throw new ArgumentNullException(nameof(context)); |
| 0 | 37 | | private readonly IAutoTaskSchedulerService _autoTaskSchedulerService = autoTaskSchedulerService ?? throw new ArgumentN |
| 0 | 38 | | private readonly IBus _bus = bus ?? throw new ArgumentNullException(nameof(bus)); |
| | 39 | |
|
| | 40 | | public async Task<(IEnumerable<AutoTaskListBusinessModel>, PaginationHelper)> GetAutoTasksAsync(int? realmId, string? |
| 0 | 41 | | { |
| 0 | 42 | | var query = _context.AutoTasks as IQueryable<AutoTask>; |
| 0 | 43 | | if (!string.IsNullOrWhiteSpace(name)) |
| 0 | 44 | | { |
| 0 | 45 | | name = name.Trim(); |
| 0 | 46 | | query = query.Where(t => t.Name != null && t.Name.Contains(name)); |
| 0 | 47 | | } |
| 0 | 48 | | if (realmId != null) |
| 0 | 49 | | { |
| 0 | 50 | | query = query.Where(t => t.RealmId == realmId); |
| 0 | 51 | | } |
| 0 | 52 | | switch (autoTaskCatrgory) |
| | 53 | | { |
| | 54 | | case AutoTaskCatrgory.Template: |
| 0 | 55 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Template); |
| 0 | 56 | | break; |
| | 57 | | case AutoTaskCatrgory.Waiting: |
| 0 | 58 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Waiting); |
| 0 | 59 | | break; |
| | 60 | | case AutoTaskCatrgory.Completed: |
| 0 | 61 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Completed); |
| 0 | 62 | | break; |
| | 63 | | case AutoTaskCatrgory.Aborted: |
| 0 | 64 | | query = query.Where(t => t.CurrentProgressId == (int)ProgressState.Aborted); |
| 0 | 65 | | break; |
| | 66 | | case AutoTaskCatrgory.Running: |
| 0 | 67 | | query = query.Where(t => t.CurrentProgressId > (int)ProgressState.Aborted); |
| 0 | 68 | | break; |
| | 69 | | } |
| 0 | 70 | | var itemCount = await query.CountAsync(); |
| 0 | 71 | | var PaginationHelper = new PaginationHelper(itemCount, pageNumber, pageSize); |
| 0 | 72 | | var autoTasks = await query.AsNoTracking() |
| 0 | 73 | | .OrderByDescending(t => t.Priority) |
| 0 | 74 | | .ThenBy(t => t.Id) |
| 0 | 75 | | .Skip(pageSize * (pageNumber - 1)) |
| 0 | 76 | | .Take(pageSize) |
| 0 | 77 | | .Select(t => new AutoTaskListBusinessModel { |
| 0 | 78 | | Id = t.Id, |
| 0 | 79 | | Name = t.Name, |
| 0 | 80 | | Priority = t.Priority, |
| 0 | 81 | | FlowId = t.Flow!.Id, |
| 0 | 82 | | FlowName = t.Flow.Name, |
| 0 | 83 | | RealmId = t.Realm.Id, |
| 0 | 84 | | RealmName = t.Realm.Name, |
| 0 | 85 | | AssignedRobotId = t.AssignedRobotId, |
| 0 | 86 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 0 | 87 | | CurrentProgressId = t.CurrentProgressId, |
| 0 | 88 | | CurrentProgressName = t.CurrentProgress.Name, |
| 0 | 89 | | }) |
| 0 | 90 | | .AsSplitQuery() |
| 0 | 91 | | .ToListAsync(); |
| 0 | 92 | | return (autoTasks, PaginationHelper); |
| 0 | 93 | | } |
| | 94 | |
|
| | 95 | | public async Task<AutoTaskBusinessModel> GetAutoTaskAsync(int autoTaskId) |
| 0 | 96 | | { |
| 0 | 97 | | return await _context.AutoTasks.AsNoTracking() |
| 0 | 98 | | .Where(t => t.Id == autoTaskId) |
| 0 | 99 | | .Include(t => t.AutoTaskDetails |
| 0 | 100 | | .OrderBy(td => td.Order)) |
| 0 | 101 | | .ThenInclude(td => td.Waypoint) |
| 0 | 102 | | .Include(t => t.AssignedRobot) |
| 0 | 103 | | .Include(t => t.CurrentProgress) |
| 0 | 104 | | .Include(t => t.Flow) |
| 0 | 105 | | .Include(t => t.Realm) |
| 0 | 106 | | .Select(t => new AutoTaskBusinessModel { |
| 0 | 107 | | Id = t.Id, |
| 0 | 108 | | Name = t.Name, |
| 0 | 109 | | Priority = t.Priority, |
| 0 | 110 | | FlowId = t.Flow!.Id, |
| 0 | 111 | | FlowName = t.Flow.Name, |
| 0 | 112 | | RealmId = t.Realm.Id, |
| 0 | 113 | | RealmName = t.Realm.Name, |
| 0 | 114 | | AssignedRobotId = t.AssignedRobotId, |
| 0 | 115 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 0 | 116 | | CurrentProgressId = t.CurrentProgressId, |
| 0 | 117 | | CurrentProgressName = t.CurrentProgress.Name, |
| 0 | 118 | | AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel { |
| 0 | 119 | | Id = td.Id, |
| 0 | 120 | | Order = td.Order, |
| 0 | 121 | | CustomX = td.CustomX, |
| 0 | 122 | | CustomY = td.CustomY, |
| 0 | 123 | | CustomRotation = td.CustomRotation, |
| 0 | 124 | | Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel { |
| 0 | 125 | | Id = td.Waypoint.Id, |
| 0 | 126 | | Name = td.Waypoint.Name, |
| 0 | 127 | | RealmId = t.Realm.Id, |
| 0 | 128 | | RealmName = t.Realm.Name, |
| 0 | 129 | | X = td.Waypoint.X, |
| 0 | 130 | | Y = td.Waypoint.Y, |
| 0 | 131 | | Rotation = td.Waypoint.Rotation, |
| 0 | 132 | | IsParking = td.Waypoint.IsParking, |
| 0 | 133 | | HasCharger = td.Waypoint.HasCharger, |
| 0 | 134 | | IsReserved = td.Waypoint.IsReserved, |
| 0 | 135 | | }, |
| 0 | 136 | | }) |
| 0 | 137 | | .OrderBy(td => td.Order) |
| 0 | 138 | | .ToList() |
| 0 | 139 | | }) |
| 0 | 140 | | .FirstOrDefaultAsync() |
| 0 | 141 | | ?? throw new LgdxNotFound404Exception(); |
| 0 | 142 | | } |
| | 143 | |
|
| | 144 | | private async Task ValidateAutoTask(HashSet<int> waypointIds, int flowId, int realmId, Guid? robotId) |
| 0 | 145 | | { |
| | 146 | | // Validate the Waypoint IDs |
| 0 | 147 | | var waypointDict = await _context.Waypoints.AsNoTracking() |
| 0 | 148 | | .Where(w => waypointIds.Contains(w.Id)) |
| 0 | 149 | | .Where(w => w.RealmId == realmId) |
| 0 | 150 | | .ToDictionaryAsync(w => w.Id, w => w); |
| 0 | 151 | | foreach(var waypointId in waypointIds) |
| 0 | 152 | | { |
| 0 | 153 | | if (!waypointDict.ContainsKey(waypointId)) |
| 0 | 154 | | { |
| 0 | 155 | | throw new LgdxValidation400Expection(nameof(Waypoint), $"The Waypoint ID {waypointId} is invalid."); |
| | 156 | | } |
| 0 | 157 | | } |
| | 158 | | // Validate the Flow ID |
| 0 | 159 | | var flow = await _context.Flows.Where(f => f.Id == flowId).AnyAsync(); |
| 0 | 160 | | if (flow == false) |
| 0 | 161 | | { |
| 0 | 162 | | throw new LgdxValidation400Expection(nameof(Flow), $"The Flow ID {flowId} is invalid."); |
| | 163 | | } |
| | 164 | | // Validate the Realm ID |
| 0 | 165 | | var realm = await _context.Realms.Where(r => r.Id == realmId).AnyAsync(); |
| 0 | 166 | | if (realm == false) |
| 0 | 167 | | { |
| 0 | 168 | | throw new LgdxValidation400Expection(nameof(Realm), $"The Realm ID {realmId} is invalid."); |
| | 169 | | } |
| | 170 | | // Validate the Assigned Robot ID |
| 0 | 171 | | if (robotId != null) |
| 0 | 172 | | { |
| 0 | 173 | | var robot = await _context |
| 0 | 174 | | .Robots |
| 0 | 175 | | .Where(r => r.Id == robotId) |
| 0 | 176 | | .Where(r => r.RealmId == realmId) |
| 0 | 177 | | .AnyAsync(); |
| 0 | 178 | | if (robot == false) |
| 0 | 179 | | { |
| 0 | 180 | | throw new LgdxValidation400Expection(nameof(Robot), $"Robot ID: {robotId} is invalid."); |
| | 181 | | } |
| 0 | 182 | | } |
| 0 | 183 | | } |
| | 184 | |
|
| | 185 | | public async Task<AutoTaskBusinessModel> CreateAutoTaskAsync(AutoTaskCreateBusinessModel autoTaskCreateBusinessModel) |
| 0 | 186 | | { |
| 0 | 187 | | HashSet<int> waypointIds = autoTaskCreateBusinessModel.AutoTaskDetails |
| 0 | 188 | | .Where(d => d.WaypointId != null) |
| 0 | 189 | | .Select(d => d.WaypointId!.Value) |
| 0 | 190 | | .ToHashSet(); |
| | 191 | |
|
| 0 | 192 | | await ValidateAutoTask(waypointIds, |
| 0 | 193 | | autoTaskCreateBusinessModel.FlowId, |
| 0 | 194 | | autoTaskCreateBusinessModel.RealmId, |
| 0 | 195 | | autoTaskCreateBusinessModel.AssignedRobotId); |
| | 196 | |
|
| 0 | 197 | | var autoTask = new AutoTask { |
| 0 | 198 | | Name = autoTaskCreateBusinessModel.Name, |
| 0 | 199 | | Priority = autoTaskCreateBusinessModel.Priority, |
| 0 | 200 | | FlowId = autoTaskCreateBusinessModel.FlowId, |
| 0 | 201 | | RealmId = autoTaskCreateBusinessModel.RealmId, |
| 0 | 202 | | AssignedRobotId = autoTaskCreateBusinessModel.AssignedRobotId, |
| 0 | 203 | | CurrentProgressId = autoTaskCreateBusinessModel.IsTemplate |
| 0 | 204 | | ? (int)ProgressState.Template |
| 0 | 205 | | : (int)ProgressState.Waiting, |
| 0 | 206 | | AutoTaskDetails = autoTaskCreateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail { |
| 0 | 207 | | Order = td.Order, |
| 0 | 208 | | CustomX = td.CustomX, |
| 0 | 209 | | CustomY = td.CustomY, |
| 0 | 210 | | CustomRotation = td.CustomRotation, |
| 0 | 211 | | WaypointId = td.WaypointId, |
| 0 | 212 | | }) |
| 0 | 213 | | .OrderBy(td => td.Order) |
| 0 | 214 | | .ToList(), |
| 0 | 215 | | }; |
| 0 | 216 | | await _context.AutoTasks.AddAsync(autoTask); |
| 0 | 217 | | await _context.SaveChangesAsync(); |
| 0 | 218 | | _autoTaskSchedulerService.ResetIgnoreRobot(autoTask.RealmId); |
| 0 | 219 | | eventService.AutoTaskHasCreated(); |
| | 220 | |
|
| 0 | 221 | | var autoTaskBusinessModel = await _context.AutoTasks.AsNoTracking() |
| 0 | 222 | | .Where(t => t.Id == autoTask.Id) |
| 0 | 223 | | .Select(t => new AutoTaskBusinessModel { |
| 0 | 224 | | Id = t.Id, |
| 0 | 225 | | Name = t.Name, |
| 0 | 226 | | Priority = t.Priority, |
| 0 | 227 | | FlowId = t.Flow!.Id, |
| 0 | 228 | | FlowName = t.Flow.Name, |
| 0 | 229 | | RealmId = t.Realm.Id, |
| 0 | 230 | | RealmName = t.Realm.Name, |
| 0 | 231 | | AssignedRobotId = t.AssignedRobotId, |
| 0 | 232 | | AssignedRobotName = t.AssignedRobot!.Name, |
| 0 | 233 | | CurrentProgressId = t.CurrentProgressId, |
| 0 | 234 | | CurrentProgressName = t.CurrentProgress.Name, |
| 0 | 235 | | AutoTaskDetails = t.AutoTaskDetails.Select(td => new AutoTaskDetailBusinessModel { |
| 0 | 236 | | Id = td.Id, |
| 0 | 237 | | Order = td.Order, |
| 0 | 238 | | CustomX = td.CustomX, |
| 0 | 239 | | CustomY = td.CustomY, |
| 0 | 240 | | CustomRotation = td.CustomRotation, |
| 0 | 241 | | Waypoint = td.Waypoint == null ? null : new WaypointBusinessModel { |
| 0 | 242 | | Id = td.Waypoint.Id, |
| 0 | 243 | | Name = td.Waypoint.Name, |
| 0 | 244 | | RealmId = t.Realm.Id, |
| 0 | 245 | | RealmName = t.Realm.Name, |
| 0 | 246 | | X = td.Waypoint.X, |
| 0 | 247 | | Y = td.Waypoint.Y, |
| 0 | 248 | | Rotation = td.Waypoint.Rotation, |
| 0 | 249 | | IsParking = td.Waypoint.IsParking, |
| 0 | 250 | | HasCharger = td.Waypoint.HasCharger, |
| 0 | 251 | | IsReserved = td.Waypoint.IsReserved, |
| 0 | 252 | | }, |
| 0 | 253 | | }) |
| 0 | 254 | | .OrderBy(td => td.Order) |
| 0 | 255 | | .ToList() |
| 0 | 256 | | }) |
| 0 | 257 | | .FirstOrDefaultAsync() |
| 0 | 258 | | ?? throw new LgdxNotFound404Exception(); |
| | 259 | |
|
| 0 | 260 | | if (autoTask.CurrentProgressId == (int)ProgressState.Waiting) |
| 0 | 261 | | { |
| 0 | 262 | | await _bus.Publish(autoTaskBusinessModel.ToContract()); |
| 0 | 263 | | } |
| | 264 | |
|
| 0 | 265 | | return autoTaskBusinessModel; |
| 0 | 266 | | } |
| | 267 | |
|
| | 268 | | public async Task<bool> UpdateAutoTaskAsync(int autoTaskId, AutoTaskUpdateBusinessModel autoTaskUpdateBusinessModel) |
| 0 | 269 | | { |
| 0 | 270 | | var task = await _context.AutoTasks |
| 0 | 271 | | .Where(t => t.Id == autoTaskId) |
| 0 | 272 | | .Include(t => t.AutoTaskDetails |
| 0 | 273 | | .OrderBy(td => td.Order)) |
| 0 | 274 | | .FirstOrDefaultAsync() |
| 0 | 275 | | ?? throw new LgdxNotFound404Exception(); |
| | 276 | |
|
| 0 | 277 | | if (task.CurrentProgressId != (int)ProgressState.Template) |
| 0 | 278 | | { |
| 0 | 279 | | throw new LgdxValidation400Expection("AutoTaskId", "Only AutoTask Templates are editable."); |
| | 280 | | } |
| | 281 | | // Ensure the input task does not include Detail ID from other task |
| 0 | 282 | | HashSet<int> dbDetailIds = task.AutoTaskDetails.Select(d => d.Id).ToHashSet(); |
| 0 | 283 | | foreach(var bmDetailId in autoTaskUpdateBusinessModel.AutoTaskDetails.Where(d => d.Id != null).Select(d => d.Id)) |
| 0 | 284 | | { |
| 0 | 285 | | if (bmDetailId != null && !dbDetailIds.Contains((int)bmDetailId)) |
| 0 | 286 | | { |
| 0 | 287 | | throw new LgdxValidation400Expection("AutoTaskDetailId", $"The Task Detail ID {(int)bmDetailId} is belongs to ot |
| | 288 | | } |
| 0 | 289 | | } |
| 0 | 290 | | HashSet<int> waypointIds = autoTaskUpdateBusinessModel.AutoTaskDetails |
| 0 | 291 | | .Where(d => d.WaypointId != null) |
| 0 | 292 | | .Select(d => d.WaypointId!.Value) |
| 0 | 293 | | .ToHashSet(); |
| 0 | 294 | | await ValidateAutoTask(waypointIds, |
| 0 | 295 | | autoTaskUpdateBusinessModel.FlowId, |
| 0 | 296 | | task.RealmId, |
| 0 | 297 | | autoTaskUpdateBusinessModel.AssignedRobotId); |
| | 298 | |
|
| 0 | 299 | | task.Name = autoTaskUpdateBusinessModel.Name; |
| 0 | 300 | | task.Priority = autoTaskUpdateBusinessModel.Priority; |
| 0 | 301 | | task.FlowId = autoTaskUpdateBusinessModel.FlowId; |
| 0 | 302 | | task.AssignedRobotId = autoTaskUpdateBusinessModel.AssignedRobotId; |
| 0 | 303 | | task.AutoTaskDetails = autoTaskUpdateBusinessModel.AutoTaskDetails.Select(td => new AutoTaskDetail { |
| 0 | 304 | | Id = (int)td.Id!, |
| 0 | 305 | | Order = td.Order, |
| 0 | 306 | | CustomX = td.CustomX, |
| 0 | 307 | | CustomY = td.CustomY, |
| 0 | 308 | | CustomRotation = td.CustomRotation, |
| 0 | 309 | | WaypointId = td.WaypointId, |
| 0 | 310 | | }).ToList(); |
| 0 | 311 | | await _context.SaveChangesAsync(); |
| 0 | 312 | | return true; |
| 0 | 313 | | } |
| | 314 | |
|
| | 315 | | public async Task<bool> DeleteAutoTaskAsync(int autoTaskId) |
| 0 | 316 | | { |
| 0 | 317 | | var autoTask = await _context.AutoTasks.AsNoTracking() |
| 0 | 318 | | .Where(t => t.Id == autoTaskId) |
| 0 | 319 | | .FirstOrDefaultAsync() |
| 0 | 320 | | ?? throw new LgdxNotFound404Exception(); |
| 0 | 321 | | if (autoTask.CurrentProgressId != (int)ProgressState.Template) |
| 0 | 322 | | { |
| 0 | 323 | | throw new LgdxValidation400Expection("AutoTaskId", "Cannot delete the task not in running status."); |
| | 324 | | } |
| | 325 | |
|
| 0 | 326 | | _context.AutoTasks.Remove(autoTask); |
| 0 | 327 | | await _context.SaveChangesAsync(); |
| | 328 | |
|
| 0 | 329 | | return true; |
| 0 | 330 | | } |
| | 331 | |
|
| | 332 | | public async Task AbortAutoTaskAsync(int autoTaskId) |
| 0 | 333 | | { |
| 0 | 334 | | var autoTask = await _context.AutoTasks.AsNoTracking() |
| 0 | 335 | | .Where(t => t.Id == autoTaskId) |
| 0 | 336 | | .FirstOrDefaultAsync() |
| 0 | 337 | | ?? throw new LgdxNotFound404Exception(); |
| | 338 | |
|
| 0 | 339 | | if (autoTask.CurrentProgressId == (int)ProgressState.Template || |
| 0 | 340 | | autoTask.CurrentProgressId == (int)ProgressState.Completed || |
| 0 | 341 | | autoTask.CurrentProgressId == (int)ProgressState.Aborted) |
| 0 | 342 | | { |
| 0 | 343 | | throw new LgdxValidation400Expection(nameof(autoTaskId), "Cannot abort the task not in running status."); |
| | 344 | | } |
| 0 | 345 | | if (autoTask.CurrentProgressId != (int)ProgressState.Waiting && |
| 0 | 346 | | autoTask.AssignedRobotId != null && |
| 0 | 347 | | await _onlineRobotsService.SetAbortTaskAsync((Guid)autoTask.AssignedRobotId!, true)) |
| 0 | 348 | | { |
| | 349 | | // If the robot is online, abort the task from the robot |
| 0 | 350 | | return; |
| | 351 | | } |
| | 352 | | else |
| 0 | 353 | | { |
| 0 | 354 | | await _autoTaskSchedulerService.AutoTaskAbortApiAsync(autoTask.Id); |
| 0 | 355 | | } |
| 0 | 356 | | } |
| | 357 | |
|
| | 358 | | public async Task AutoTaskNextApiAsync(Guid robotId, int taskId, string token) |
| 0 | 359 | | { |
| 0 | 360 | | var result = await _autoTaskSchedulerService.AutoTaskNextApiAsync(robotId, taskId, token) |
| 0 | 361 | | ?? throw new LgdxValidation400Expection(nameof(token), "The next token is invalid."); |
| 0 | 362 | | _onlineRobotsService.SetAutoTaskNextApi(robotId, result); |
| 0 | 363 | | } |
| | 364 | | } |