using Newtonsoft.Json; using WIDESEA_Core; using WIDESEAWCS_Common; using WIDESEAWCS_Common.HttpEnum; using WIDESEAWCS_Common.TaskEnum; using WIDESEAWCS_Core; using WIDESEAWCS_Core.Helper; using WIDESEAWCS_Core.LogHelper; using WIDESEAWCS_DTO.Stock; using WIDESEAWCS_DTO.TaskInfo; using WIDESEAWCS_ITaskInfoService; using WIDESEAWCS_Model.Models; using WIDESEAWCS_QuartzJob; using WIDESEAWCS_Tasks.Workflow.Abstractions; namespace WIDESEAWCS_Tasks { /// /// 机器人任务处理器 - 负责任务获取、下发、入库任务回传及库存 DTO 构建 /// /// /// 核心职责: /// 1. 从数据库轮询待处理的机器人任务 /// 2. 向机器人客户端下发取货指令(Pickbattery) /// 3. 处理入库任务的回传(拆盘/组盘/换盘场景) /// 4. 构建库存回传 DTO 并调用 WMS 接口 /// /// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。 /// public class RobotTaskProcessor { /// /// Socket 客户端网关接口 /// /// /// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。 /// 提供统一的客户端通信接口。 /// private readonly ISocketClientGateway _socketClientGateway; /// /// 机械手状态管理器 /// private readonly RobotStateManager _stateManager; /// /// 机器人任务服务 /// /// /// 用于查询、更新、删除机器人任务记录。 /// private readonly IRobotTaskService _robotTaskService; /// /// 通用任务服务 /// /// /// 用于与 WMS 系统交互,接收任务、处理任务状态等。 /// private readonly ITaskService _taskService; /// /// HTTP 客户端帮助类 /// /// /// 用于调用 WMS 系统的 HTTP 接口。 /// private readonly HttpClientHelper _httpClientHelper; /// /// 构造函数 /// /// Socket 网关 /// 状态管理器 /// 机器人任务服务 /// 通用任务服务 /// HTTP 客户端帮助类 public RobotTaskProcessor( ISocketClientGateway socketClientGateway, RobotStateManager stateManager, IRobotTaskService robotTaskService, ITaskService taskService, HttpClientHelper httpClientHelper) { _socketClientGateway = socketClientGateway; _stateManager = stateManager; _robotTaskService = robotTaskService; _taskService = taskService; _httpClientHelper = httpClientHelper; } /// /// 按设备编码获取当前机器人的待处理任务 /// /// /// 从数据库中查询指定设备编码的待处理机器人任务。 /// 只返回状态为"待处理"的任务。 /// /// 机器人设备信息,包含设备编码 /// 待处理的任务对象,如果没有则返回 null public Dt_RobotTask? GetTask(RobotCraneDevice robotCrane) { return _robotTaskService.QueryRobotCraneTask(robotCrane.DeviceCode); } /// /// 删除机器人任务 /// /// /// 当任务完成(无论是成功还是失败)时调用,删除数据库中的任务记录。 /// /// 要删除的任务 ID /// 删除是否成功 public bool? DeleteTask(int ID) { return _robotTaskService.Repository.DeleteDataById(ID); } /// /// 下发取货指令(Pickbattery)到机器人客户端 /// /// /// 发送格式:Pickbattery,{源地址} /// 例如:Pickbattery,A01 表示从 A01 位置取货 /// /// 下发成功后: /// 1. 更新任务状态为"机器人执行中" /// 2. 将任务关联到状态对象 /// 3. 安全更新状态到 Redis /// 4. 更新任务记录到数据库 /// /// 要下发的任务对象 /// 机器人当前状态 public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state) { // 构建取货指令,格式:Pickbattery,{源地址} string taskString = $"Pickbattery,{task.RobotSourceAddress}"; // 通过 Socket 网关发送指令到机器人客户端 bool result = await _socketClientGateway.SendToClientAsync(state.IPAddress, taskString); if (result) { // 发送成功,记录日志 QuartzLogger.Error($"下发取货指令,指令: {taskString}", state.RobotCrane.DeviceName); // 更新任务状态为"机器人执行中" task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode(); // 将任务关联到状态对象 state.CurrentTask = task; // 保持原语义:仅在状态安全写入成功后再更新任务状态 // 这样可以确保状态和任务记录的一致性 if (_stateManager.TryUpdateStateSafely(state.IPAddress, state)) { await _robotTaskService.UpdateRobotTaskAsync(task); } } } /// /// 处理入库任务回传(拆盘/组盘/换盘场景) /// /// /// 当取货完成(AllPickFinished)或放货完成(AllPutFinished)时调用此方法。 /// 根据任务类型和地址来源决定如何回传给 WMS。 /// /// 处理逻辑: /// 1. 根据 useSourceAddress 决定使用源地址还是目标地址 /// 2. 根据任务类型(组盘/换盘/拆盘)决定任务类型(入库/空托盘入库) /// 3. 构建 CreateTaskDto 并调用 WMS 接口创建任务 /// 4. 接收 WMS 返回的任务信息 /// 5. 更新输送线的目标地址、任务号等 /// /// 机器人当前状态 /// 是否使用源地址(true 表示拆盘/换盘场景,false 表示组盘/换盘场景) /// 处理是否成功 public async Task HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress) { // 获取当前关联的任务 var currentTask = state.CurrentTask; if (currentTask == null) { return false; } // 获取巷道代码 string roadway = currentTask.RobotSourceAddressLineCode; // 根据巷道名称判断仓库 ID // ZYRB1 -> 1, HPRB001 -> 2, 其他 -> 3 int warehouseId = currentTask.RobotRoadway == "ZYRB1" ? 1 : currentTask.RobotRoadway == "HPRB001" ? 2 : 3; // 任务类型(0 表示未定义,稍后根据任务类型设置) int taskType = 0; // 源地址和目标地址(初始化) string SourceAddress = currentTask.RobotTargetAddressLineCode; string TargetAddress = currentTask.RobotSourceAddressLineCode; // 托盘代码(初始化为空) string PalletCode = string.Empty; // 获取任务类型的枚举值 var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType; // 根据 useSourceAddress 决定处理逻辑 if (useSourceAddress) { // 使用源地址的场景:拆盘、换盘(放空托盘) switch (robotTaskType) { case RobotTaskTypeEnum.GroupPallet: // 组盘任务不使用源地址,直接返回 false return false; case RobotTaskTypeEnum.ChangePallet: case RobotTaskTypeEnum.SplitPallet: // 换盘/拆盘场景:托盘需要入库 taskType = TaskTypeEnum.InEmpty.GetHashCode(); // 空托盘入库 PalletCode = currentTask.RobotSourceAddressPalletCode; // 使用源地址的托盘码 break; } } else { // 使用目标地址的场景:组盘、换盘(成品入库) switch (robotTaskType) { case RobotTaskTypeEnum.ChangePallet: case RobotTaskTypeEnum.GroupPallet: // 换盘/组盘场景:货物需要入库 taskType = TaskTypeEnum.Inbound.GetHashCode(); // 成品入库 PalletCode = currentTask.RobotTargetAddressPalletCode; // 使用目标地址的托盘码 break; case RobotTaskTypeEnum.SplitPallet: // 拆盘任务不使用目标地址 return true; } } // 构建创建任务的 DTO CreateTaskDto taskDto = new CreateTaskDto { PalletCode = PalletCode, // 托盘条码 SourceAddress = SourceAddress ?? string.Empty, // 源地址 TargetAddress = TargetAddress ?? string.Empty, // 目标地址 Roadway = roadway, // 巷道 WarehouseId = warehouseId, // 仓库 ID PalletType = 1, // 托盘类型(默认为1) TaskType = taskType // 任务类型(入库/空托盘入库) }; // 调用 WMS 接口创建入库任务 var result = _httpClientHelper.Post(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson()); // 如果调用失败或返回错误状态 if (!result.Data.Status && result.IsSuccess) { return false; } // 解析 WMS 返回的任务信息 WMSTaskDTO taskDTO = JsonConvert.DeserializeObject(result.Data.Data.ToJson() ?? string.Empty) ?? new WMSTaskDTO(); // 调用任务服务接收 WMS 任务 var content = _taskService.ReceiveWMSTask(new List { taskDTO }); if (!content.Status) { return false; } // 解析返回的任务信息 var taskInfo = JsonConvert.DeserializeObject(content.Data.ToJson() ?? string.Empty) ?? new Dt_Task(); // 获取源地址 string sourceAddress = taskDTO.SourceAddress; // 查找源地址对应的输送线设备 IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceAddress)); if (device != null) { // 将设备转换为输送线类型 CommonConveyorLine conveyorLine = (CommonConveyorLine)device; // 设置输送线的目标地址 conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress); // 设置输送线的任务号 conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress); // 触发输送线开始执行(写入 WCS_STB = 1) conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_STB, 1, sourceAddress); // 更新任务状态到下一阶段 if (_taskService.UpdateTaskStatusToNext(taskInfo).Status) { return true; } } return false; } /// /// 构建库存回传 DTO /// /// /// 用于拆盘和组盘操作时,向 WMS 回传库存信息。 /// DTO 包含源货位、目标货位、托盘码以及每个位置的电池条码。 /// /// 机器人当前状态 /// 电池位置数组 /// 构建好的库存 DTO public static StockDTO BuildStockDTO(RobotSocketState state, int[] positions) { return new StockDTO { // 源输送线编号 SourceLineNo = state.CurrentTask.RobotSourceAddressLineCode, // 源托盘号 SourcePalletNo = state.CurrentTask.RobotSourceAddressPalletCode, // 目标托盘号 TargetPalletNo = state.CurrentTask.RobotTargetAddressPalletCode, // 目标输送线编号 TargetLineNo = state.CurrentTask.RobotTargetAddressLineCode, // 电池位置详情列表 // 过滤掉位置为 0 或负数的无效数据 // 按位置编号排序 // 为每个位置生成对应的库存详情 Details = positions .Where(x => x > 0) // 过滤无效位置 .OrderBy(x => x) // 按位置排序 .Select((x, idx) => new StockDetailDTO { // 数量:如果已有任务总数,使用任务总数+当前位置数;否则只使用当前位置数 Quantity = state.RobotTaskTotalNum > 0 ? state.RobotTaskTotalNum + positions.Length : positions.Length, // 通道/位置编号 Channel = x, // 电池条码:如果状态中有条码列表,取对应位置的条码;否则为空 CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : "" }) .ToList() }; } /// /// 调用拆盘 API /// /// /// 当取货完成且需要拆盘时调用。 /// 将电池从托盘上取下,逐个放置到目标位置。 /// /// 库存 DTO,包含要拆盘的电芯信息 /// HTTP 响应结果 public HttpResponseResult PostSplitPalletAsync(StockDTO stockDTO) { return _httpClientHelper.Post(nameof(ConfigKey.SplitPalletAsync), stockDTO.ToJson()); } /// /// 调用组盘/换盘 API /// /// /// 当放货完成且需要组盘或换盘时调用。 /// 将多个电池组合到同一个托盘上。 /// /// configKey 参数决定调用哪个 API: /// - GroupPalletAsync: 组盘接口 /// - ChangePalletAsync: 换盘接口 /// /// 配置键名,决定调用哪个 API /// 库存 DTO,包含要组盘的电芯信息 /// HTTP 响应结果 public HttpResponseResult PostGroupPalletAsync(string configKey, StockDTO stockDTO) { return _httpClientHelper.Post(configKey, stockDTO.ToJson()); } } }