| | |
| | | using Quartz; |
| | | using System.Collections.Concurrent; |
| | | using System.Net.Sockets; |
| | | using Microsoft.Extensions.Logging; |
| | | using Quartz; |
| | | using System.Net; |
| | | using WIDESEA_Core; |
| | | using WIDESEAWCS_Core.Caches; |
| | | using WIDESEAWCS_Core.Helper; |
| | | using WIDESEAWCS_DTO.Stock; |
| | | using WIDESEAWCS_ITaskInfoRepository; |
| | | using WIDESEAWCS_Core.LogHelper; |
| | | using WIDESEAWCS_ITaskInfoService; |
| | | using WIDESEAWCS_Model.Models; |
| | | using WIDESEAWCS_QuartzJob; |
| | | using WIDESEAWCS_QuartzJob.Service; |
| | | using WIDESEAWCS_Tasks.SocketServer; |
| | | using WIDESEAWCS_Tasks.Workflow; |
| | | using WIDESEAWCS_Tasks.Workflow.Abstractions; |
| | | |
| | | namespace WIDESEAWCS_Tasks |
| | | { |
| | | /// <summary> |
| | | /// 机器人任务作业(Quartz Job)- 负责调度与生命周期管理 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// Quartz 定时任务,每秒执行一次(默认),主要职责: |
| | | /// 1. 从 JobDataMap 获取设备信息 |
| | | /// 2. 确保 TCP 客户端已连接并订阅消息 |
| | | /// 3. 轮询待处理的机器人任务 |
| | | /// 4. 调用工作流编排器执行任务状态机 |
| | | /// |
| | | /// 使用 [DisallowConcurrentExecution] 禁止并发执行,确保同一设备的任务串行处理。 |
| | | /// 具体的状态机流程逻辑委托给 <see cref="RobotWorkflowOrchestrator"/> 处理。 |
| | | /// </remarks> |
| | | [DisallowConcurrentExecution] |
| | | public class RobotJob : IJob |
| | | { |
| | | private readonly TcpSocketServer _TcpSocket; |
| | | private static readonly ConcurrentDictionary<string, RobotSocketState> _socketStates = new(); |
| | | private static int _eventSubscribedFlag; |
| | | private readonly IRobotTaskService _taskService; |
| | | private readonly ITaskExecuteDetailService _taskExecuteDetailService; |
| | | private readonly ITaskRepository _taskRepository; |
| | | private readonly IRouterService _routerService; |
| | | /// <summary> |
| | | /// 任务总数上限 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 当机器人处理的货物数量达到此上限时,不再下发新任务。 |
| | | /// 防止机器人过度劳累或系统过载。 |
| | | /// </remarks> |
| | | private const int MaxTaskTotalNum = 48; |
| | | |
| | | public RobotJob(TcpSocketServer TcpSocket, IRobotTaskService taskService) |
| | | /// <summary> |
| | | /// 消息事件订阅标志 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 使用原子操作确保全局只订阅一次 TCP 消息事件。 |
| | | /// 防止多个 Job 实例重复订阅导致消息被多次处理。 |
| | | /// </remarks> |
| | | private static int _messageSubscribedFlag; |
| | | |
| | | /// <summary> |
| | | /// 机械手客户端连接管理器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 负责管理 TCP 连接的生命周期,包括连接、断开、消息发送等。 |
| | | /// </remarks> |
| | | private readonly RobotClientManager _clientManager; |
| | | |
| | | /// <summary> |
| | | /// 机械手状态管理器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 负责管理 Redis 缓存中的机械手状态,包括读写和并发控制。 |
| | | /// </remarks> |
| | | private readonly RobotStateManager _stateManager; |
| | | |
| | | /// <summary> |
| | | /// 消息路由器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 负责处理从 TCP 服务器接收到的消息,并分发给合适的处理器。 |
| | | /// 是消息处理管道的入口。 |
| | | /// </remarks> |
| | | private readonly IRobotMessageRouter _messageRouter; |
| | | |
| | | /// <summary> |
| | | /// 机器人任务处理器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 负责任务的下发、状态更新、与 WMS 的交互等。 |
| | | /// </remarks> |
| | | private readonly RobotTaskProcessor _taskProcessor; |
| | | |
| | | /// <summary> |
| | | /// 机器人工作流编排器 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 负责执行任务的状态机流转,根据当前状态决定下一步动作。 |
| | | /// 这是核心的业务逻辑编排组件。 |
| | | /// </remarks> |
| | | private readonly IRobotWorkflowOrchestrator _workflowOrchestrator; |
| | | |
| | | /// <summary> |
| | | /// 日志记录器 |
| | | /// </summary> |
| | | private readonly ILogger<RobotJob> _logger; |
| | | |
| | | /// <summary> |
| | | /// 构造函数 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 采用依赖注入方式获取所需服务,并完成组件的初始化和组装。 |
| | | /// 这里体现了"控制反转"和"依赖注入"的设计原则。 |
| | | /// </remarks> |
| | | /// <param name="tcpSocket">TCP Socket 服务器实例</param> |
| | | /// <param name="robotTaskService">机器人任务服务</param> |
| | | /// <param name="taskService">通用任务服务</param> |
| | | /// <param name="cache">缓存服务</param> |
| | | /// <param name="httpClientHelper">HTTP 客户端帮助类,用于调用 WMS 接口</param> |
| | | /// <param name="logger">日志记录器</param> |
| | | public RobotJob( |
| | | TcpSocketServer tcpSocket, |
| | | IRobotTaskService robotTaskService, |
| | | ITaskService taskService, |
| | | ICacheService cache, |
| | | HttpClientHelper httpClientHelper, |
| | | ILogger<RobotJob> logger) |
| | | { |
| | | _TcpSocket = TcpSocket; |
| | | _taskService = taskService; |
| | | // 初始化状态管理器,传入缓存服务 |
| | | _stateManager = new RobotStateManager(cache, _logger); |
| | | _logger = logger; |
| | | |
| | | // 创建 Socket 网关,封装 TcpSocketServer 的访问 |
| | | // 后续替换通信实现时只需替换网关层 |
| | | ISocketClientGateway socketGateway = new SocketClientGateway(tcpSocket); |
| | | |
| | | // 初始化任务处理器 |
| | | _taskProcessor = new RobotTaskProcessor(socketGateway, _stateManager, robotTaskService, taskService, httpClientHelper, _logger); |
| | | |
| | | // 初始化客户端管理器 |
| | | _clientManager = new RobotClientManager(tcpSocket, _stateManager, _logger); |
| | | |
| | | // 初始化命令处理器 |
| | | // 简单命令处理器:处理状态更新等简单命令 |
| | | var simpleCommandHandler = new RobotSimpleCommandHandler(_taskProcessor); |
| | | // 前缀命令处理器:处理 pickfinished、putfinished 等带参数的命令 |
| | | var prefixCommandHandler = new RobotPrefixCommandHandler(robotTaskService, _taskProcessor, _stateManager, socketGateway); |
| | | |
| | | // 初始化消息路由器 |
| | | _messageRouter = new RobotMessageHandler(socketGateway, _stateManager, cache, simpleCommandHandler, prefixCommandHandler, logger); |
| | | |
| | | // 初始化工作流编排器 |
| | | _workflowOrchestrator = new RobotWorkflowOrchestrator(_stateManager, _clientManager, _taskProcessor, robotTaskService, _logger); |
| | | |
| | | // 订阅客户端断开连接事件 |
| | | _clientManager.OnClientDisconnected += OnClientDisconnected; |
| | | |
| | | // 全局只订阅一次 TCP 消息事件(保持原有行为) |
| | | // 使用 Interlocked.CompareExchange 实现原子操作 |
| | | if (System.Threading.Interlocked.CompareExchange(ref _messageSubscribedFlag, 1, 0) == 0) |
| | | { |
| | | // 将消息路由器的处理方法绑定到 TCP 服务器的消息接收事件 |
| | | tcpSocket.MessageReceived += _messageRouter.HandleMessageReceivedAsync; |
| | | _logger.LogError("机器手TCP消息事件已订阅"); |
| | | QuartzLogger.Error($"机器手TCP消息事件已订阅"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 客户端断开连接的事件处理 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 当客户端断开连接时记录日志,便于排查问题。 |
| | | /// </remarks> |
| | | /// <param name="sender">事件发送者</param> |
| | | /// <param name="state">断开连接的机械手状态</param> |
| | | private void OnClientDisconnected(object? sender, RobotSocketState state) |
| | | { |
| | | _logger.LogError("客户端已断开连接"); |
| | | QuartzLogger.Error($"客户端已断开连接", state.RobotCrane.DeviceName); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// Quartz Job 的执行入口 |
| | | /// </summary> |
| | | /// <remarks> |
| | | /// 执行流程: |
| | | /// 1. 从 JobDataMap 获取设备信息 |
| | | /// 2. 确保客户端已连接并订阅消息 |
| | | /// 3. 轮询待处理任务 |
| | | /// 4. 调用工作流编排器执行任务 |
| | | /// |
| | | /// 注意:此方法可能频繁调用(每秒一次),需注意性能。 |
| | | /// </remarks> |
| | | /// <param name="context">Quartz 作业执行上下文,包含作业详情和数据</param> |
| | | public async Task Execute(IJobExecutionContext context) |
| | | { |
| | | // 从 JobDataMap 中获取作业参数 |
| | | bool flag = context.JobDetail.JobDataMap.TryGetValue("JobParams", out object? value); |
| | | |
| | | // 将参数转换为机器人设备信息 |
| | | RobotCraneDevice robotCrane = (RobotCraneDevice?)value ?? new RobotCraneDevice(); |
| | | |
| | | // 如果没有获取到有效的设备信息,直接返回 |
| | | if (!flag || robotCrane.IsNullOrEmpty()) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 获取设备 IP 地址,作为状态缓存的键 |
| | | string ipAddress = robotCrane.IPAddress; |
| | | |
| | | // 获取或创建状态 |
| | | RobotSocketState state = _socketStates.GetOrAdd(ipAddress, _ => new RobotSocketState |
| | | { |
| | | IPAddress = ipAddress, |
| | | RobotCrane = robotCrane |
| | | }); |
| | | // 获取或创建设备状态对象 |
| | | RobotSocketState state = _stateManager.GetOrCreateState(ipAddress, robotCrane); |
| | | |
| | | // 更新设备信息 |
| | | // 更新设备基础信息(以防设备信息在运行期间发生变化) |
| | | state.RobotCrane = robotCrane; |
| | | |
| | | // 检查是否有该客户端连接 |
| | | var clientIds = _TcpSocket.GetClientIds(); |
| | | if (!clientIds.Contains(ipAddress)) |
| | | try |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 订阅一次 message 事件(全局一次) |
| | | if (Interlocked.CompareExchange(ref _eventSubscribedFlag, 1, 0) == 0) |
| | | { |
| | | _TcpSocket.MessageReceived += _TcpSocket_MessageReceived; |
| | | _TcpSocket.RobotReceived += _TcpSocket_RobotReceived; |
| | | } |
| | | |
| | | if (!state.IsEventSubscribed) |
| | | { |
| | | _TcpSocket._clients.TryGetValue(ipAddress, out TcpClient client); |
| | | Task clientTask = _TcpSocket.HandleClientAsync(client, robotCrane.IPAddress, _TcpSocket._cts.Token, state); |
| | | state.IsEventSubscribed = true; |
| | | } |
| | | |
| | | // 获取任务并缓存到状态中 |
| | | Dt_RobotTask? task = GetTask(robotCrane); |
| | | if (task != null) |
| | | { |
| | | state.CurrentTask = task; |
| | | if (task.RobotTaskTotalNum != 48) |
| | | // 确保客户端已连接并订阅消息事件 |
| | | // 如果客户端未连接或订阅失败,直接返回等待下次调度 |
| | | if (!_clientManager.EnsureClientSubscribed(ipAddress, robotCrane)) |
| | | { |
| | | // 处理正在执行的任务 |
| | | if (state.RobotRunMode == 1 && state.RobotControlMode == 1) |
| | | { |
| | | await Task.Delay(1000); |
| | | if ((state.CurrentAction == "Homed" || state.CurrentAction == "PickFinished" || state.CurrentAction == "PutFinished") && state.OperStatus == "Running") |
| | | { |
| | | // TODO 读取线体电池条码,发送取电池指令 |
| | | if (true) |
| | | { |
| | | // 模拟读取条码 |
| | | state.CellBarcode = new string[] { "CellBarcode1", "CellBarcode2", "CellBarcode3", "CellBarcode4" }; |
| | | return; |
| | | } |
| | | |
| | | string taskString = $"Pickbattery,{task.RobotSourceAddress}"; |
| | | // 发送任务指令 |
| | | bool result = await _TcpSocket.SendToClientAsync(ipAddress, taskString); |
| | | } |
| | | } |
| | | // 轮询获取该设备的待处理任务 |
| | | var task = _taskProcessor.GetTask(robotCrane); |
| | | |
| | | // 如果有待处理任务 |
| | | if (task != null) |
| | | { |
| | | // 获取最新的设备状态 |
| | | var latestState = _stateManager.GetState(ipAddress); |
| | | if (latestState == null) |
| | | { |
| | | // 状态不存在,可能设备未初始化 |
| | | return; |
| | | } |
| | | |
| | | // 检查任务总数是否未达到上限 |
| | | if (latestState.RobotTaskTotalNum < MaxTaskTotalNum) |
| | | { |
| | | // 调用工作流编排器执行任务 |
| | | // 编排器会根据当前状态决定下一步动作 |
| | | await _workflowOrchestrator.ExecuteAsync(latestState, task, ipAddress); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 事件:客户端断开连接时触发 |
| | | /// </summary> |
| | | /// <param name="clientId"></param> |
| | | /// <returns></returns> |
| | | private Task<string?> _TcpSocket_RobotReceived(string clientId) |
| | | { |
| | | _socketStates.TryRemove(clientId, out _); |
| | | return Task.FromResult<string?>(null); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 事件:收到消息时触发 |
| | | /// </summary> |
| | | /// <param name="message"></param> |
| | | /// <param name="isJson"></param> |
| | | /// <param name="client"></param> |
| | | /// <param name="state"></param> |
| | | /// <returns></returns> |
| | | private async Task<string?> _TcpSocket_MessageReceived(string message, bool isJson, TcpClient client, RobotSocketState state) |
| | | { |
| | | string messageLower = message.ToLowerInvariant(); |
| | | |
| | | if (IsSimpleCommand(messageLower, state)) |
| | | catch (Exception ex) |
| | | { |
| | | return null; |
| | | // 异常处理已在组件内部进行,Job 层保持兜底语义 |
| | | // 记录异常而不是静默吞掉,便于排查问题 |
| | | _logger?.LogError(ex, "RobotJob执行异常,IP: {IpAddress}", ipAddress); |
| | | QuartzLogger.Error($"RobotJob执行异常,IP: {ipAddress}", state.RobotCrane.DeviceName, ex); |
| | | } |
| | | |
| | | if (IsPrefixCommand(messageLower)) |
| | | { |
| | | try |
| | | { |
| | | var parts = message.Split(','); |
| | | if (parts.Length >= 1) |
| | | { |
| | | var cmd = parts[0].ToLowerInvariant(); |
| | | int[] positions = new int[4]; |
| | | for (int i = 1; i <= 4 && i < parts.Length; i++) |
| | | { |
| | | int.TryParse(parts[i], out positions[i - 1]); |
| | | } |
| | | |
| | | if (cmd.StartsWith("pickfinished")) |
| | | { |
| | | state.LastPickPositions = positions; |
| | | state.CurrentAction = "PickFinished"; |
| | | } |
| | | else if (cmd.StartsWith("putfinished")) |
| | | { |
| | | state.LastPutPositions = positions; |
| | | // 发送数据给WMS组盘/换盘 |
| | | StockDTO stockDTO = new StockDTO |
| | | { |
| | | SourceLineNo = state.CurrentTask?.RobotSourceAddressLineCode, |
| | | SourcePalletNo = state.CurrentTask?.RobotSourceAddressPalletCode, |
| | | TargetPalletNo = state.CurrentTask?.RobotTargetAddressPalletCode, |
| | | TargetLineNo = state.CurrentTask?.RobotTargetAddressLineCode, |
| | | Details = positions |
| | | .Where(x => x > 0) |
| | | .OrderBy(x => x) |
| | | .Select((x, idx) => new StockDetailDTO |
| | | { |
| | | Quantity = state.CurrentTask?.RobotTaskTotalNum ?? 1, |
| | | Channel = x > 0 ? x : throw new ArgumentOutOfRangeException(nameof(x), "Channel must be positive"), |
| | | CellBarcode = state.CellBarcode[idx] |
| | | }) |
| | | .ToList() |
| | | }; |
| | | state.CurrentAction = "PutFinished"; |
| | | } |
| | | } |
| | | } |
| | | catch { } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 机械手简单命令处理 |
| | | /// </summary> |
| | | /// <param name="message"></param> |
| | | /// <param name="state"></param> |
| | | /// <returns></returns> |
| | | private bool IsSimpleCommand(string message, RobotSocketState state) |
| | | { |
| | | switch (message) |
| | | { |
| | | case "homing": |
| | | state.CurrentAction = "Homing"; |
| | | return true; |
| | | |
| | | case "homed": |
| | | state.CurrentAction = "Homed"; |
| | | return true; |
| | | |
| | | case "picking": |
| | | state.CurrentAction = "Picking"; |
| | | return true; |
| | | |
| | | case "puting": |
| | | state.CurrentAction = "Putting"; |
| | | return true; |
| | | |
| | | case "allpickfinished": |
| | | state.CurrentAction = "AllPickFinished"; |
| | | if(state.CurrentTask?.RobotTaskType == 2|| state.CurrentTask?.RobotTaskType == 3) |
| | | { |
| | | // TODO 机械手取货完成,判断是否换盘、拆盘任务,创建空托盘回库任务 |
| | | } |
| | | return true; |
| | | |
| | | case "allputfinished": |
| | | state.CurrentAction = "AllPutFinished"; |
| | | if (state.CurrentTask?.RobotTaskType == 1 ) |
| | | { |
| | | // TODO 机械手取货完成,判断是否组盘任务,创建组盘入库任务 |
| | | } |
| | | return true; |
| | | |
| | | case "running": |
| | | state.OperStatus = "Running"; |
| | | return true; |
| | | |
| | | case "pausing": |
| | | state.OperStatus = "Pausing"; |
| | | return true; |
| | | |
| | | case "warming": |
| | | state.OperStatus = "Warming"; |
| | | return true; |
| | | |
| | | case "emstoping": |
| | | state.OperStatus = "Emstoping"; |
| | | return true; |
| | | |
| | | case "runmode,1": |
| | | state.RobotRunMode = 1; |
| | | return true; |
| | | |
| | | case "runmode,2": |
| | | state.RobotRunMode = 2; |
| | | return true; |
| | | |
| | | case "controlmode,1": |
| | | state.RobotControlMode = 1; |
| | | return true; |
| | | |
| | | case "controlmode,2": |
| | | state.RobotControlMode = 2; |
| | | return true; |
| | | |
| | | case "armobject,1": |
| | | state.RobotArmObject = 1; |
| | | return true; |
| | | |
| | | case "armobject,0": |
| | | state.RobotArmObject = 0; |
| | | return true; |
| | | |
| | | default: |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 机械手前缀命令处理 |
| | | /// </summary> |
| | | /// <param name="message"></param> |
| | | /// <returns></returns> |
| | | private static bool IsPrefixCommand(string message) |
| | | { |
| | | return message.StartsWith("pickfinished") || message.StartsWith("putfinished"); |
| | | } |
| | | |
| | | private Dt_RobotTask? GetTask(RobotCraneDevice robotCrane) |
| | | { |
| | | return _taskService.QueryRobotCraneTask(robotCrane.DeviceCode); |
| | | } |
| | | } |
| | | |
| | | public class RobotSocketState |
| | | { |
| | | public string IPAddress { get; set; } = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 是否已订阅消息事件 |
| | | /// </summary> |
| | | public bool IsEventSubscribed { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 机械手运行模式 |
| | | /// </summary> |
| | | public int? RobotRunMode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 机械手控制模式 |
| | | /// </summary> |
| | | public int? RobotControlMode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 机械手抓取对象 |
| | | /// </summary> |
| | | public int? RobotArmObject { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 机械手设备信息 |
| | | /// </summary> |
| | | public RobotCraneDevice? RobotCrane { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 当前动作 |
| | | /// </summary> |
| | | public string? CurrentAction { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 当前状态 |
| | | /// </summary> |
| | | public string? OperStatus { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 取货完成位置 |
| | | /// </summary> |
| | | public int[]? LastPickPositions { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 放货完成位置 |
| | | /// </summary> |
| | | public int[]? LastPutPositions { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 抓取位置条码 |
| | | /// </summary> |
| | | public string[] CellBarcode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 当前抓取任务 |
| | | /// </summary> |
| | | public Dt_RobotTask? CurrentTask { get; set; } |
| | | } |
| | | } |
| | | } |