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
|
{
|
/// <summary>
|
/// 机器人任务处理器 - 负责任务获取、下发、入库任务回传及库存 DTO 构建
|
/// </summary>
|
/// <remarks>
|
/// 核心职责:
|
/// 1. 从数据库轮询待处理的机器人任务
|
/// 2. 向机器人客户端下发取货指令(Pickbattery)
|
/// 3. 处理入库任务的回传(拆盘/组盘/换盘场景)
|
/// 4. 构建库存回传 DTO 并调用 WMS 接口
|
///
|
/// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。
|
/// </remarks>
|
public class RobotTaskProcessor
|
{
|
/// <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>
|
/// <param name="socketClientGateway">Socket 网关</param>
|
/// <param name="stateManager">状态管理器</param>
|
/// <param name="robotTaskService">机器人任务服务</param>
|
/// <param name="taskService">通用任务服务</param>
|
/// <param name="httpClientHelper">HTTP 客户端帮助类</param>
|
public RobotTaskProcessor(
|
ISocketClientGateway socketClientGateway,
|
RobotStateManager stateManager,
|
IRobotTaskService robotTaskService,
|
ITaskService taskService,
|
HttpClientHelper httpClientHelper)
|
{
|
_socketClientGateway = socketClientGateway;
|
_stateManager = stateManager;
|
_robotTaskService = robotTaskService;
|
_taskService = taskService;
|
_httpClientHelper = httpClientHelper;
|
}
|
|
/// <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>
|
/// 当任务完成(无论是成功还是失败)时调用,删除数据库中的任务记录。
|
/// </remarks>
|
/// <param name="ID">要删除的任务 ID</param>
|
/// <returns>删除是否成功</returns>
|
public bool? DeleteTask(int ID)
|
{
|
return _robotTaskService.Repository.DeleteDataById(ID);
|
}
|
|
/// <summary>
|
/// 下发取货指令(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)
|
{
|
// 发送成功,记录日志
|
QuartzLogger.Error($"下发取货指令,指令: {taskString}", state.RobotCrane.DeviceName);
|
|
// 更新任务状态为"机器人执行中"
|
task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
|
|
// 将任务关联到状态对象
|
state.CurrentTask = task;
|
|
// 保持原语义:仅在状态安全写入成功后再更新任务状态
|
// 这样可以确保状态和任务记录的一致性
|
if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
|
{
|
await _robotTaskService.UpdateRobotTaskAsync(task);
|
}
|
}
|
}
|
|
/// <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)
|
{
|
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<WebResponseContent>(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson());
|
|
// 如果调用失败或返回错误状态
|
if (!result.Data.Status && result.IsSuccess)
|
{
|
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)
|
{
|
return false;
|
}
|
|
// 解析返回的任务信息
|
var taskInfo = JsonConvert.DeserializeObject<Dt_Task>(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;
|
}
|
|
/// <summary>
|
/// 构建库存回传 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,
|
|
// 电池位置详情列表
|
// 过滤掉位置为 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()
|
};
|
}
|
|
/// <summary>
|
/// 调用拆盘 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
|
/// </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());
|
}
|
}
|
}
|