using AutoMapper; using HslCommunication; using NetTaste; using Newtonsoft.Json; using Quartz; using SqlSugar; using System.Reflection; using System.Text; using WIDESEAWCS_BasicInfoRepository; using WIDESEAWCS_BasicInfoService; using WIDESEAWCS_Common; using WIDESEAWCS_Common.TaskEnum; using WIDESEAWCS_Core; using WIDESEAWCS_Core.Helper; using WIDESEAWCS_Core.HttpContextUser; using WIDESEAWCS_DTO.TaskInfo; using WIDESEAWCS_IBasicInfoRepository; using WIDESEAWCS_IBasicInfoService; using WIDESEAWCS_ISystemServices; using WIDESEAWCS_ITaskInfoRepository; using WIDESEAWCS_ITaskInfoService; using WIDESEAWCS_Model.Models; using WIDESEAWCS_QuartzJob; using WIDESEAWCS_QuartzJob.DeviceBase; using WIDESEAWCS_QuartzJob.DTO; using WIDESEAWCS_QuartzJob.Models; using WIDESEAWCS_QuartzJob.Repository; using WIDESEAWCS_QuartzJob.Service; using WIDESEAWCS_SignalR; using WIDESEAWCS_Tasks.ConveyorLineJob; using ICacheService = WIDESEAWCS_Core.Caches.ICacheService; namespace WIDESEAWCS_Tasks { [DisallowConcurrentExecution] public partial class CommonConveyorLineJob : JobBase, IJob { private readonly ITaskService _taskService; private readonly ITaskRepository _taskRepository; private readonly ITask_HtyRepository _task_HtyRepository; private readonly ITaskExecuteDetailService _taskExecuteDetailService; private readonly IRouterService _routerService; private readonly ISys_ConfigService _sys_ConfigService; private readonly IMapper _mapper; private readonly IDt_StationManagerService _stationManagerService; private readonly IDt_StationManagerRepository _stationManagerRepository; private readonly ICacheService _cacheService; private readonly INoticeService _noticeService; private readonly IDeviceInfoRepository _deviceInfoRepository; private static List? userTokenIds; private static List? userIds; public CommonConveyorLineJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, ITaskRepository taskRepository,ISys_ConfigService sys_ConfigService, IDt_StationManagerService stationManagerService, IDt_StationManagerRepository stationManagerRepository, ICacheService cacheService, INoticeService noticeService, IDeviceInfoRepository deviceInfoRepository, ITask_HtyRepository task_HtyRepository) { _taskService = taskService; _taskExecuteDetailService = taskExecuteDetailService; _routerService = routerService; _mapper = mapper; _taskRepository = taskRepository; _sys_ConfigService = sys_ConfigService; _stationManagerService = stationManagerService; _stationManagerRepository = stationManagerRepository; _cacheService = cacheService; _noticeService = noticeService; _deviceInfoRepository = deviceInfoRepository; _task_HtyRepository = task_HtyRepository; } public async Task Execute(IJobExecutionContext context) { string jobName = context.JobDetail.Key.Name; try { // 从JobDataMap中获取传递的参数 CommonConveyorLine conveyorLine = (CommonConveyorLine)context.JobDetail.JobDataMap.Get("JobParams"); if (conveyorLine != null) { // 查询所有子设备的位置 List childDeviceCodes = _routerService.QueryAllPositions(conveyorLine.DeviceCode); // 获取所有站点管理器 List stationManagers = _stationManagerService.GetAllStationByDeviceCode(conveyorLine.DeviceCode); // 并行处理每个子设备 var tasks = childDeviceCodes.Select(childDeviceCode => ProcessDeviceAsync(conveyorLine, childDeviceCode)).ToList(); // 并行处理每个站点管理器 tasks = stationManagers.Select(station => Task.Run(async () => { ConveyorLineTaskCommand command = ReadCommand(conveyorLine, station.stationChildCode); if (command == null ) { return; } IStationHandler handler = StationHandlerFactory.GetHandler(station.stationType, this); await handler.HandleStationAsync(conveyorLine, station, command); })).ToList(); await Task.WhenAll(tasks); } } catch (Exception ex) { // 输出异常信息 Console.Out.WriteLine(nameof(CommonConveyorLineJob) + ":" + ex.ToString()); } return; } private Task ProcessDeviceAsync(CommonConveyorLine conveyorLine, string childDeviceCode) { try { // 读取任务命令和设备命令 ConveyorLineTaskCommand command = ReadCommand(conveyorLine, childDeviceCode); if (command != null) { #region 调用事件总线通知前端 // 获取缓存中的用户信息 var tokenInfos = _cacheService.Get>("Cache_UserToken"); if (tokenInfos != null && tokenInfos.Any()) { userTokenIds = tokenInfos.Select(x => x.Token_ID).ToList(); userIds = tokenInfos.Select(x => x.UserId).ToList(); // 构造通知数据 object obj = new { command }; // 发送通知 _noticeService.LineData(userIds.FirstOrDefault(), userTokenIds, new { conveyorLine.DeviceName, childDeviceCode, data = obj }); } #endregion 调用事件总线通知前端 var structs = BitConverter.GetBytes(command.InteractiveSignal).Reverse().ToArray().ToBoolArray(); // 获取设备协议详情 List? deviceProtocolDetails = conveyorLine.DeviceProtocolDetailDTOs.Where(x => x.DeviceProParamName == nameof(ConveyorLineTaskCommand.InteractiveSignal)).ToList(); if (deviceProtocolDetails != null) { foreach (var item in deviceProtocolDetails) { int itemValue = item.ProtocalDetailValue.ObjToInt(); if (structs[itemValue] == true) { // 获取处理方法 MethodInfo? method = GetType().GetMethod(item.ProtocolDetailType); if (method != null) { // 调用处理方法 method.Invoke(this, new object[] { conveyorLine, command, childDeviceCode, itemValue }); } } } } } } catch (Exception ex) { } return Task.CompletedTask; } #region 入库 /// /// 输送线请求入库 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 /// 线体当前bool读取偏移地址 public async Task RequestInbound(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode, int ProtocalDetailValue) { try { // 输出警告信息,表示任务已到达子设备并请求扫码入库 var log = $"时间:【{DateTime.Now}】【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】请求扫码入库"; ConsoleHelper.WriteWarningLine(log); // 发送通知 await _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); // 查询条码对应的任务 var task = _taskService.QueryBarCodeConveyorLineTask(command.Barcode.ToString(), childDeviceCode); if (task != null) { // 如果任务类型是出库或出托盘,则处理出库任务 if (task.TaskType == (int)TaskOutboundTypeEnum.OutTray || task.TaskType == (int)TaskOutboundTypeEnum.Outbound) { HandleTaskOut(conveyorLine, command, childDeviceCode, ProtocalDetailValue, task); } else { // 获取任务的下一目标地址 var next = task.NextAddress; // 将任务映射为命令 var taskCommand = MapTaskCommand(task, command); // 恢复任务的下一目标地址 task.NextAddress = next; // 发送命令到子设备 conveyorLine.SendCommand(taskCommand, childDeviceCode); // 输出警告信息,表示任务已到达子设备并请求扫码入库,下一目标地址 var logs = $"时间:【{DateTime.Now}】【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】请求扫码入库,下一目标地址【{taskCommand.TargetAddress}】"; ConsoleHelper.WriteWarningLine(logs); await _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); // 发送任务完成通知 ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); // 更新任务状态为下一状态 _taskService.UpdateTaskStatusToNext(task); } } else { // 如果任务为空且条码不为"NoRead"且条码不为空,则处理新任务 if (task == null && command.Barcode.ToString() != "NoRead" && command.Barcode.IsNotEmptyOrNull()) { // 查询条码对应的任务 task = _taskService.QueryBarcodeTask(command.Barcode.ToString(), childDeviceCode); if (task == null) // 异步处理新任务 await HandleNewTaskAsync(conveyorLine, command, childDeviceCode, ProtocalDetailValue); } } } catch (Exception ex) { // 捕获并输出异常信息 Console.Out.WriteLine(ex.ToString()); } } #region 输送线请求入库下一地址 /// /// 输送线请求入库下一地址 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 public void RequestInNextAddress(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode) { var log = $"时间:【{DateTime.Now}】【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】请求入库下一地址"; ConsoleHelper.WriteWarningLine(log); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); Dt_Task task = _taskService.QueryExecutingConveyorLineTask(command.TaskNum, childDeviceCode, command.Barcode.ToString()); if (task != null) { if (command.Barcode.ToString() == task.PalletCode) { Dt_Task? newTask = _taskService.UpdatePosition(task.TaskNum, task.CurrentAddress); if (newTask != null) { //ConveyorLineTaskCommand taskCommand = _mapper.Map(newTask); //taskCommand.InteractiveSignal = command.InteractiveSignal; var next = newTask.NextAddress; var taskCommand = MapTaskCommand(newTask, command); newTask.NextAddress = next; var logs = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】请求入库下一地址,下一目标地址【{taskCommand.TargetAddress}】"; ConsoleHelper.WriteWarningLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); conveyorLine.SendCommand(taskCommand, childDeviceCode); _taskService.UpdateData(newTask); } } } } #endregion 输送线请求入库下一地址 /// /// 输送线入库完成 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 /// 线体当前bool读取偏移地址 public void ConveyorLineInFinish(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode, int ProtocalDetailValue) { try { var log = $"时间:【{DateTime.Now}】【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线入库完成"; ConsoleHelper.WriteWarningLine(log); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); var task = _taskService.QueryExecutingConveyorLineTask(command.TaskNum, childDeviceCode, command.Barcode.ToString()); if (task != null && task.TaskState != (int)TaskInStatusEnum.Line_InFinish) { if (command.Barcode.ToString() == task.PalletCode && childDeviceCode == task.NextAddress) { if (task.TaskType == (int)TaskInboundTypeEnum.InNG) { int nextStatus = task.TaskState.GetNextNotCompletedStatus(); var station = _stationManagerRepository.QueryFirst(x => x.stationChildCode == task.SourceAddress); task.CurrentAddress = station.stationLocation; task.NextAddress = station.stationNGLocation; task.TargetAddress = task.NextAddress; task.TaskState = nextStatus; task.ModifyDate = DateTime.Now; task.Modifier = "System"; _taskRepository.UpdateData(task); conveyorLine.SetValue(ConveyorLineDBName.WriteConveyorLineTargetAddress, "1000", childDeviceCode); var logs = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线入库完成,下一目标地址【等待分配货位,并写入1000】"; ConsoleHelper.WriteWarningLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); } else { WebResponseContent content = _taskService.UpdateTaskStatusToNext(task); WriteInfo(conveyorLine.DeviceName, content.ToJsonString()); if (!content.Status) { ConsoleHelper.WriteWarningLine($"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线入库完成,任务执行失败{JsonConvert.SerializeObject(content)}"); return; } conveyorLine.SetValue(ConveyorLineDBName.WriteConveyorLineTargetAddress, "1000", childDeviceCode); var logs = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线入库完成,下一目标地址【等待分配货位,并写入1000】"; ConsoleHelper.WriteWarningLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); Console.Out.WriteLine(content.ToJsonString()); } } } } catch (Exception ex) { } } #endregion 入库 #region 出库 /// /// 输送线请求出信息 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 /// 线体当前bool读取偏移地址 public void RequestOutbound(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode, int ProtocalDetailValue) { try { // 查询输送线任务,根据输送线设备和子设备代码获取任务信息 var task = _taskService.QueryConveyorLineTask(conveyorLine.DeviceCode, childDeviceCode); // 输出成功信息,包括输送线名称、任务号、托盘条码和子设备代码,以及任务信息 var logs = $"时间:【{DateTime.Now}】【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线请求出库,task{task.ToJsonString()}"; ConsoleHelper.WriteSuccessLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); // 如果任务不为空,则执行以下操作 if (task != null) { // 获取任务的下一目标地址 var next = task.NextAddress; // 将任务命令映射到当前任务 var taskCommand = MapTaskCommand(task, command); // 恢复任务的下一目标地址 task.NextAddress = next; // 输出成功信息,包括输送线名称、任务号、托盘条码、子设备代码和下一目标地址 var log = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线请求出库,下一目标地址【{taskCommand.TargetAddress}】"; ConsoleHelper.WriteSuccessLine(log); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); // 向输送线发送命令 conveyorLine.SendCommand(taskCommand, childDeviceCode); // 完成输送线发送任务,并更新任务状态 ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); // 更新任务状态到下一个状态 _taskService.UpdateTaskStatusToNext(task); // 如果任务的目标地址是"1020-1",则再次更新任务状态到下一个状态 if (task.TargetAddress == "1020-1"|| task.TargetAddress == "1049-8") { _taskService.UpdateTaskStatusToNext(task); } } } catch (Exception ex) { } } /// /// 输送线请求出库下一地址 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 public void RequestOutNextAddress(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode, int ProtocalDetailValue) { // 打印成功日志,显示当前设备名称、任务号、托盘条码以及请求出库下一地址的子设备代码 var log = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线请求出库下一地址"; ConsoleHelper.WriteSuccessLine(log); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); // 查询正在执行的输送线任务,根据任务号和子设备代码获取任务信息 Dt_Task task = _taskService.QueryExecutingConveyorLineTask(command.TaskNum, childDeviceCode, command.Barcode.ToString()); // 如果任务存在 if (task != null) { // 检查任务中的托盘条码是否与命令中的托盘条码一致 if (command.Barcode.ToString() == task.PalletCode) { // 更新任务的位置信息,并获取更新后的任务对象 Dt_Task? newTask = _taskService.UpdatePosition(task.TaskNum, task.CurrentAddress); // 如果更新后的任务对象不为空 if (newTask != null) { // 获取下一目标地址 var next = newTask.NextAddress; // 将新任务对象映射为任务命令对象 var taskCommand = MapTaskCommand(newTask, command); // 恢复新任务对象的下一目标地址 newTask.NextAddress = next; // 打印成功日志,显示当前设备名称、任务号、托盘条码、子设备代码以及下一目标地址 var logs = $"【{conveyorLine._deviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线请求出库下一地址,下一目标地址【{taskCommand.TargetAddress}】"; ConsoleHelper.WriteSuccessLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); // 发送任务命令到子设备 conveyorLine.SendCommand(taskCommand, childDeviceCode); // 标记输送线发送任务完成 ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); _taskService.UpdateData(newTask); } } } } /// /// 输送线出库完成 /// /// 输送线实例对象 /// 读取的请求信息 /// 子设备编号 public void ConveyorLineOutFinish(CommonConveyorLine conveyorLine, ConveyorLineTaskCommand command, string childDeviceCode, int ProtocalDetailValue) { try { // 打印成功信息,表示托盘已到达指定输送线并完成出库 var logs = $"【{conveyorLine.DeviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线出库完成"; ConsoleHelper.WriteSuccessLine(logs); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = logs, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, logs); // 查询正在执行的输送线任务 var task = _taskService.QueryExecutingConveyorLineTask(command.TaskNum, childDeviceCode, command.Barcode.ToString()); // 如果任务存在 if (task != null) { // 如果任务中的托盘条码与命令中的托盘条码一致 if (command.Barcode.ToString() == task.PalletCode) { // 创建一个空的WebResponseContent对象 WebResponseContent content = new WebResponseContent(); // 保存任务的下一目标地址 var next = task.NextAddress; // 将任务映射为命令 var taskCommand = MapTaskCommand(task, command); // 恢复任务的下一目标地址 task.NextAddress = next; // 如果任务的托盘条码与命令中的托盘条码不一致或者任务备注为"NG" if (task.PalletCode != command.Barcode.ToString() || task.Remark == "NG") { // 设置命令的目标地址为NG地址 taskCommand.TargetAddress = 1; } else { // 设置命令的目标地址为1000 taskCommand.TargetAddress = 1000; } // 打印成功信息,表示托盘已到达指定输送线并完成出库,下一目标地址已确定 var log = $"【{conveyorLine.DeviceName}】任务号:【{command.TaskNum}】,托盘条码:【{command.Barcode}】已到达【{childDeviceCode}】输送线出库完成,下一目标地址【{taskCommand.TargetAddress}】"; ConsoleHelper.WriteSuccessLine(log); _noticeService.Logs(userTokenIds, new { conveyorLine.DeviceName, log = log, time = DateTime.Now.ToString("G"), color = "red" }); WriteInfo(conveyorLine.DeviceName, log); // 发送命令到输送线 conveyorLine.SendCommand(taskCommand, childDeviceCode); // 完成输送线发送 ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); // 更新任务状态到下一个状态 content = _taskService.UpdateTaskStatusToNext(task); } else { // 完成输送线发送 ConveyorLineSendFinish(conveyorLine, childDeviceCode, ProtocalDetailValue, true); // 更新任务状态到下一个状态 _taskService.UpdateTaskStatusToNext(task); } } } catch (Exception ex) { } } #endregion 出库 /// /// 输送线交互完成 /// /// 输送线实例对象 /// 子设备编号 /// 线体当前bool读取偏移地址 /// 值 public void ConveyorLineSendFinish(CommonConveyorLine conveyorLine, string childDeviceCode, int ProtocalDetailValue, bool value) { // 从conveyorLine中的DeviceProDTOs列表中查找第一个符合条件的DeviceProDTO对象 // 条件是DeviceProParamType等于DeviceCommand,且DeviceChildCode等于childDeviceCode // 查找结果按DeviceProOffset升序排列 DeviceProDTO? devicePro = conveyorLine.DeviceProDTOs.Where(x => x.DeviceProParamType == nameof(DeviceCommand) && x.DeviceChildCode == childDeviceCode).OrderBy(x => x.DeviceProOffset).FirstOrDefault(); // 将devicePro的DeviceProAddress按'.'分割成字符串数组x string[] x = devicePro.DeviceProAddress.Split('.'); // 将数组x的最后一个元素替换为ProtocalDetailValue加1后的字符串形式 x[x.Length - 1] = (ProtocalDetailValue + 1).ToString(); // 将修改后的数组x重新拼接成字符串,作为新的DeviceProAddress string DeviceProAddress = string.Join(".", x); var writeBool = conveyorLine.Communicator.Read(DeviceProAddress); if (writeBool != value) { // 使用conveyorLine的Communicator对象的Write方法,将value写入新的DeviceProAddress地址 conveyorLine.Communicator.Write(DeviceProAddress, value); } } public async Task LogAndWarn(string deviceName, string log, string color = "red") { ConsoleHelper.WriteWarningLine(log); await _noticeService.Logs(userTokenIds, new { deviceName, log = log, time = DateTime.Now.ToString("G"), color = color }); WriteInfo(deviceName, log); } private ConveyorLineTaskCommand ReadCommand(CommonConveyorLine conveyorLine,string childDeviceCode) { ConveyorLineTaskCommand command = conveyorLine.ReadCustomer(childDeviceCode); DeviceProDTO? devicePro = conveyorLine.DeviceProDTOs.Where(x => x.DeviceChildCode == childDeviceCode && x.DeviceProParamName == "ConveyorLineBarcode").FirstOrDefault(); byte[] Barcode = conveyorLine.Communicator.Read(devicePro.DeviceProAddress, 10); command.Barcode = Encoding.ASCII.GetString(Barcode); return command; } } }