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