| | |
| | | import com.zy.common.service.CommonService; |
| | | import com.zy.common.utils.NavigateUtils; |
| | | import com.zy.common.utils.RedisUtil; |
| | | import com.zy.core.move.StationMoveCoordinator; |
| | | import com.zy.core.move.StationMoveDispatchMode; |
| | | import com.zy.core.move.StationMoveSession; |
| | | import com.zy.core.News; |
| | | import com.zy.core.cache.MessageQueue; |
| | | import com.zy.core.cache.SlaveConnection; |
| | |
| | | import com.zy.core.model.Task; |
| | | import com.zy.core.model.command.StationCommand; |
| | | import com.zy.core.model.protocol.StationProtocol; |
| | | import com.zy.core.model.protocol.StationTaskBufferItem; |
| | | import com.zy.core.service.StationTaskLoopService; |
| | | import com.zy.core.thread.StationThread; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Component; |
| | |
| | | public class StationOperateProcessUtils { |
| | | private static final int LOOP_LOAD_RESERVE_EXPIRE_SECONDS = 120; |
| | | private static final int OUT_ORDER_DISPATCH_LIMIT_SECONDS = 2; |
| | | private static final int STATION_COMMAND_DISPATCH_DEDUP_SECONDS = 10; |
| | | private static final int STATION_IDLE_RECOVER_SECONDS = 10; |
| | | private static final int STATION_IDLE_RECOVER_LIMIT_SECONDS = 10; |
| | | private static final int STATION_IDLE_RECOVER_LIMIT_SECONDS = 30; |
| | | private static final int STATION_IDLE_TRACK_EXPIRE_SECONDS = 60 * 60; |
| | | private static final long STATION_MOVE_RESET_WAIT_MS = 1000L; |
| | | private static final String IDLE_RECOVER_CLEARED_MEMO = "idleRecoverRerouteCleared"; |
| | |
| | | private StationPathPolicyService stationPathPolicyService; |
| | | @Autowired |
| | | private BasStationOptService basStationOptService; |
| | | @Autowired |
| | | private StationTaskLoopService stationTaskLoopService; |
| | | @Autowired |
| | | private WrkAnalysisService wrkAnalysisService; |
| | | @Autowired |
| | | private StationMoveCoordinator stationMoveCoordinator; |
| | | |
| | | //执行输送站点入库任务 |
| | | public synchronized void stationInExecute() { |
| | |
| | | continue; |
| | | } |
| | | |
| | | if (wrkMast.getWrkSts() == WrkStsType.INBOUND_DEVICE_RUN.sts) { |
| | | if (!Objects.equals(wrkMast.getWrkSts(), WrkStsType.NEW_INBOUND.sts)) { |
| | | continue; |
| | | } |
| | | |
| | |
| | | continue; |
| | | } |
| | | |
| | | wrkMast.setWrkSts(WrkStsType.INBOUND_DEVICE_RUN.sts); |
| | | Date now = new Date(); |
| | | wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts); |
| | | wrkMast.setSourceStaNo(stationProtocol.getStationId()); |
| | | wrkMast.setStaNo(targetStationId); |
| | | wrkMast.setSystemMsg(""); |
| | | wrkMast.setIoTime(new Date()); |
| | | wrkMast.setIoTime(now); |
| | | wrkMast.setModiTime(now); |
| | | if (wrkMastService.updateById(wrkMast)) { |
| | | MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command)); |
| | | wrkAnalysisService.markInboundStationStart(wrkMast, now); |
| | | boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "stationInExecute"); |
| | | if (offered && stationMoveCoordinator != null) { |
| | | // 初始入库命令也纳入 session 跟踪,后续停留恢复/绕圈/堵塞重算才能基于同一条路线状态判断。 |
| | | stationMoveCoordinator.recordDispatch( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | "stationInExecute", |
| | | command, |
| | | false |
| | | ); |
| | | } |
| | | News.info("输送站点入库命令下发成功,站点号={},工作号={},命令数据={}", stationId, wrkMast.getWrkNo(), JSON.toJSONString(command)); |
| | | redisUtil.set(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId, "lock", 5); |
| | | loadGuardState.reserveLoopTask(loopHitResult.getLoopNo()); |
| | |
| | | && stationProtocol.isLoading() |
| | | && stationProtocol.getTaskNo() == 0 |
| | | ) { |
| | | // 先算当前任务在批次出库中的路径倾向系数,再带着这个系数去决策目标站, |
| | | // 这样同一批次不同序号任务在排序点、绕圈点和堵塞重算时会得到一致的目标裁决。 |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision( |
| | | stationProtocol.getStationId(), |
| | | wrkMast, |
| | | outOrderList |
| | | outOrderList, |
| | | pathLenFactor |
| | | ); |
| | | Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | if (moveStaNo == null) { |
| | |
| | | } |
| | | |
| | | DispatchLimitConfig limitConfig = getDispatchLimitConfig(stationProtocol.getStationId(), moveStaNo); |
| | | LoopHitResult loopHitResult = findPathLoopHit(limitConfig, stationProtocol.getStationId(), moveStaNo, loadGuardState); |
| | | LoopHitResult loopHitResult = findPathLoopHit(limitConfig, stationProtocol.getStationId(), moveStaNo, loadGuardState, wrkMast, pathLenFactor); |
| | | |
| | | if (isDispatchBlocked(limitConfig, currentStationTaskCountRef[0], loadGuardState, loopHitResult.isThroughLoop())) { |
| | | return; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), moveStaNo, 0); |
| | | StationCommand command = buildOutboundMoveCommand( |
| | | stationThread, |
| | | wrkMast, |
| | | stationProtocol.getStationId(), |
| | | moveStaNo, |
| | | pathLenFactor |
| | | ); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败"); |
| | | continue; |
| | | } |
| | | |
| | | Date now = new Date(); |
| | | wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts); |
| | | wrkMast.setSystemMsg(""); |
| | | wrkMast.setIoTime(new Date()); |
| | | wrkMast.setIoTime(now); |
| | | wrkMast.setModiTime(now); |
| | | if (wrkMastService.updateById(wrkMast)) { |
| | | MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command)); |
| | | wrkAnalysisService.markOutboundStationStart(wrkMast, now); |
| | | boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "crnStationOutExecute"); |
| | | if (offered && stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.recordDispatch( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | "crnStationOutExecute", |
| | | command, |
| | | false |
| | | ); |
| | | } |
| | | News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command)); |
| | | redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5); |
| | | redisUtil.del(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo()); |
| | |
| | | && stationProtocol.isLoading() |
| | | && stationProtocol.getTaskNo() == 0 |
| | | ) { |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), wrkMast.getStaNo(), 0); |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | StationCommand command = buildOutboundMoveCommand( |
| | | stationThread, |
| | | wrkMast, |
| | | stationProtocol.getStationId(), |
| | | wrkMast.getStaNo(), |
| | | pathLenFactor |
| | | ); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败"); |
| | | continue; |
| | |
| | | wrkMast.setSystemMsg(""); |
| | | wrkMast.setIoTime(new Date()); |
| | | if (wrkMastService.updateById(wrkMast)) { |
| | | MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command)); |
| | | boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "dualCrnStationOutExecute"); |
| | | if (offered && stationMoveCoordinator != null) { |
| | | // 双工位堆垛机转入输送线后同样要登记 session,否则后续重算只能看到 PLC 命令,看不到路线语义。 |
| | | stationMoveCoordinator.recordDispatch( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | "dualCrnStationOutExecute", |
| | | command, |
| | | false |
| | | ); |
| | | } |
| | | notifyUtils.notify(String.valueOf(SlaveType.Devp), stationObjModel.getDeviceNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN, null); |
| | | News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command)); |
| | | redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5); |
| | |
| | | if (wrkMast == null || wrkMast.getWrkNo() == null) { |
| | | return; |
| | | } |
| | | if (stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.finishSession(wrkMast.getWrkNo()); |
| | | } |
| | | Date now = new Date(); |
| | | wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts); |
| | | wrkMast.setIoTime(new Date()); |
| | | wrkMast.setIoTime(now); |
| | | wrkMast.setModiTime(now); |
| | | wrkMastService.updateById(wrkMast); |
| | | wrkAnalysisService.markOutboundStationComplete(wrkMast, now); |
| | | if (deviceNo != null) { |
| | | notifyUtils.notify(String.valueOf(SlaveType.Devp), deviceNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null); |
| | | } |
| | |
| | | } |
| | | |
| | | if (complete) { |
| | | if (stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.finishSession(wrkNo); |
| | | } |
| | | wrkMast.setWrkSts(WrkStsType.COMPLETE_OUTBOUND.sts); |
| | | wrkMast.setIoTime(new Date()); |
| | | wrkMastService.updateById(wrkMast); |
| | |
| | | } |
| | | redisUtil.set(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo(), "lock", 15); |
| | | |
| | | if (wrkMast.getIoType() == WrkIoType.IN.id && runBlockReassignLocStationList.contains(stationProtocol.getStationId())) { |
| | | //站点处于重新分配库位区域 |
| | | //运行堵塞,重新申请任务 |
| | | String response = wmsOperateUtils.applyReassignTaskLocNo(wrkMast.getWrkNo(), stationProtocol.getStationId()); |
| | | if (Cools.isEmpty(response)) { |
| | | News.taskError(wrkMast.getWrkNo(), "请求WMS重新分配库位接口失败,接口未响应!!!response:{}", response); |
| | | continue; |
| | | } |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (jsonObject.getInteger("code").equals(200)) { |
| | | StartupDto dto = jsonObject.getObject("data", StartupDto.class); |
| | | |
| | | String sourceLocNo = wrkMast.getLocNo(); |
| | | String locNo = dto.getLocNo(); |
| | | |
| | | LocMast sourceLocMast = locMastService.queryByLoc(sourceLocNo); |
| | | if (sourceLocMast == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位信息不存在", sourceLocNo); |
| | | continue; |
| | | } |
| | | |
| | | if (!sourceLocMast.getLocSts().equals("S")) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位状态不处于入库预约", sourceLocNo); |
| | | continue; |
| | | } |
| | | |
| | | LocMast locMast = locMastService.queryByLoc(locNo); |
| | | if (locMast == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位信息不存在", locNo); |
| | | continue; |
| | | } |
| | | |
| | | if (!locMast.getLocSts().equals("O")) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位状态不处于空库位", locNo); |
| | | continue; |
| | | } |
| | | |
| | | FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo); |
| | | if (findCrnNoResult == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | Integer crnNo = findCrnNoResult.getCrnNo(); |
| | | |
| | | Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationProtocol.getStationId()); |
| | | if (targetStationId == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationProtocol.getStationId()); |
| | | continue; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), targetStationId, 0); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo()); |
| | | continue; |
| | | } |
| | | |
| | | //更新源库位 |
| | | sourceLocMast.setLocSts("O"); |
| | | sourceLocMast.setModiTime(new Date()); |
| | | locMastService.updateById(sourceLocMast); |
| | | |
| | | //更新目标库位 |
| | | locMast.setLocSts("S"); |
| | | locMast.setModiTime(new Date()); |
| | | locMastService.updateById(locMast); |
| | | |
| | | //更新工作档数据 |
| | | wrkMast.setLocNo(locNo); |
| | | wrkMast.setStaNo(targetStationId); |
| | | |
| | | if (findCrnNoResult.getCrnType().equals(SlaveType.Crn)) { |
| | | wrkMast.setCrnNo(crnNo); |
| | | } else if (findCrnNoResult.getCrnType().equals(SlaveType.DualCrn)) { |
| | | wrkMast.setDualCrnNo(crnNo); |
| | | } else { |
| | | throw new CoolException("未知设备类型"); |
| | | } |
| | | |
| | | if (wrkMastService.updateById(wrkMast)) { |
| | | MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command)); |
| | | } |
| | | } else { |
| | | News.error("请求WMS接口失败!!!response:{}", response); |
| | | } |
| | | } else { |
| | | //运行堵塞,重新计算路线 |
| | | OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision( |
| | | stationProtocol.getStationId(), |
| | | wrkMast, |
| | | outOrderStationIds |
| | | ); |
| | | Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | if (moveStaNo == null || Objects.equals(moveStaNo, stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getRunBlockRerouteCommand( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | moveStaNo, |
| | | 0 |
| | | ); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), |
| | | "输送站点堵塞重规划未找到可下发路线,当前站点={},目标站点={}", |
| | | stationProtocol.getStationId(), |
| | | moveStaNo); |
| | | continue; |
| | | } |
| | | |
| | | MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command)); |
| | | syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), outOrderStationIds, dispatchDecision, command); |
| | | News.info("输送站点堵塞后重新计算路径命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command)); |
| | | if (shouldUseRunBlockDirectReassign(wrkMast, stationProtocol.getStationId(), runBlockReassignLocStationList)) { |
| | | executeRunBlockDirectReassign(basDevp, stationThread, stationProtocol, wrkMast); |
| | | continue; |
| | | } |
| | | |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | // 运行堵塞不单独决定业务目标站,仍然复用出库排序/绕圈的目标裁决, |
| | | // 这里只是要求用 run-block 专用算路,并在重发前清掉旧 session/segment 状态。 |
| | | RerouteContext context = RerouteContext.create( |
| | | RerouteSceneType.RUN_BLOCK_REROUTE, |
| | | basDevp, |
| | | stationThread, |
| | | stationProtocol, |
| | | wrkMast, |
| | | outOrderStationIds, |
| | | pathLenFactor, |
| | | "checkStationRunBlock_reroute" |
| | | ).withRunBlockCommand() |
| | | .withSuppressDispatchGuard() |
| | | .withCancelSessionBeforeDispatch() |
| | | .withResetSegmentCommandsBeforeDispatch(); |
| | | executeSharedReroute(context); |
| | | } |
| | | } |
| | | } |
| | |
| | | continue; |
| | | } |
| | | |
| | | // 排序点本身已经堵塞时,不在 out-order 里做二次决策,统一交给 run-block 重规划处理。 |
| | | if (stationProtocol.isRunBlock()) { |
| | | continue; |
| | | } |
| | | |
| | | if (!stationProtocol.getStationId().equals(stationProtocol.getTargetStaNo())) { |
| | | continue; |
| | | } |
| | |
| | | if (Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) { |
| | | continue; |
| | | } |
| | | |
| | | if (isWatchingCircleArrival(wrkMast.getWrkNo(), stationProtocol.getStationId())) { |
| | | // 只有活动中的现有路线才会压制 out-order;BLOCKED 路线要允许排序点重新启动。 |
| | | if (shouldSkipOutOrderDispatchForExistingRoute(wrkMast.getWrkNo(), stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | |
| | | OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision( |
| | | stationProtocol.getStationId(), |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | RerouteContext context = RerouteContext.create( |
| | | RerouteSceneType.OUT_ORDER, |
| | | basDevp, |
| | | stationThread, |
| | | stationProtocol, |
| | | wrkMast, |
| | | outOrderStationIds |
| | | ); |
| | | Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | if (moveStaNo == null || Objects.equals(moveStaNo, stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), moveStaNo, 0); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败"); |
| | | continue; |
| | | } |
| | | if (!tryAcquireOutOrderDispatchLock(wrkMast.getWrkNo(), stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), outOrderStationIds, dispatchDecision, command); |
| | | MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command)); |
| | | News.info(dispatchDecision.isCircle() ? "{}任务进行绕圈" : "{}任务直接去目标点", wrkMast.getWrkNo()); |
| | | outOrderStationIds, |
| | | pathLenFactor, |
| | | "checkStationOutOrder" |
| | | ).withDispatchDeviceNo(stationObjModel.getDeviceNo()) |
| | | .withSuppressDispatchGuard() |
| | | .withOutOrderDispatchLock() |
| | | .withResetSegmentCommandsBeforeDispatch(); |
| | | executeSharedReroute(context); |
| | | } |
| | | } |
| | | } |
| | |
| | | continue; |
| | | } |
| | | |
| | | StationCommand circleCommand = getWatchCircleCommand(stationProtocol.getTaskNo()); |
| | | if (circleCommand == null) { |
| | | continue; |
| | | } |
| | | if (!stationProtocol.getStationId().equals(circleCommand.getTargetStaNo())) { |
| | | // 绕圈触发优先读 session 的下一决策站,legacy WATCH_CIRCLE key 只做兼容回退。 |
| | | if (!isWatchingCircleArrival(stationProtocol.getTaskNo(), stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | |
| | |
| | | if (Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) { |
| | | continue; |
| | | } |
| | | |
| | | OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision( |
| | | stationProtocol.getStationId(), |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | RerouteContext context = RerouteContext.create( |
| | | RerouteSceneType.WATCH_CIRCLE, |
| | | basDevp, |
| | | stationThread, |
| | | stationProtocol, |
| | | wrkMast, |
| | | outOrderList |
| | | ); |
| | | Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | if (moveStaNo == null || Objects.equals(moveStaNo, stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), moveStaNo, 0); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败"); |
| | | continue; |
| | | } |
| | | if (!tryAcquireOutOrderDispatchLock(wrkMast.getWrkNo(), stationProtocol.getStationId())) { |
| | | continue; |
| | | } |
| | | syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), outOrderList, dispatchDecision, command); |
| | | MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command)); |
| | | outOrderList, |
| | | pathLenFactor, |
| | | "watchCircleStation" |
| | | ).withSuppressDispatchGuard() |
| | | .withOutOrderDispatchLock() |
| | | .withResetSegmentCommandsBeforeDispatch(); |
| | | executeSharedReroute(context); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private StationCommand buildOutboundMoveCommand(StationThread stationThread, |
| | | WrkMast wrkMast, |
| | | Integer stationId, |
| | | Integer targetStationId, |
| | | Double pathLenFactor) { |
| | | if (stationThread == null || wrkMast == null) { |
| | | return null; |
| | | } |
| | | return stationThread.getCommand( |
| | | StationCommandType.MOVE, |
| | | wrkMast.getWrkNo(), |
| | | stationId, |
| | | targetStationId, |
| | | 0, |
| | | normalizePathLenFactor(pathLenFactor) |
| | | ); |
| | | } |
| | | |
| | | RerouteCommandPlan buildRerouteCommandPlan(RerouteContext context, |
| | | RerouteDecision decision) { |
| | | if (context == null) { |
| | | return RerouteCommandPlan.skip("missing-context"); |
| | | } |
| | | if (decision == null) { |
| | | return RerouteCommandPlan.skip("missing-decision"); |
| | | } |
| | | if (decision.skip()) { |
| | | return RerouteCommandPlan.skip(decision.skipReason()); |
| | | } |
| | | if (context.stationThread() == null || context.stationProtocol() == null || context.wrkMast() == null) { |
| | | return RerouteCommandPlan.skip("missing-runtime-dependency"); |
| | | } |
| | | Integer currentStationId = context.stationProtocol().getStationId(); |
| | | Integer targetStationId = decision.targetStationId(); |
| | | if (currentStationId == null || targetStationId == null) { |
| | | return RerouteCommandPlan.skip("missing-target-station"); |
| | | } |
| | | if (Objects.equals(currentStationId, targetStationId)) { |
| | | return RerouteCommandPlan.skip("same-station"); |
| | | } |
| | | |
| | | StationCommand command = context.useRunBlockCommand() |
| | | ? context.stationThread().getRunBlockRerouteCommand( |
| | | context.wrkMast().getWrkNo(), |
| | | currentStationId, |
| | | targetStationId, |
| | | 0, |
| | | context.pathLenFactor() |
| | | ) |
| | | : buildOutboundMoveCommand( |
| | | context.stationThread(), |
| | | context.wrkMast(), |
| | | currentStationId, |
| | | targetStationId, |
| | | context.pathLenFactor() |
| | | ); |
| | | if (command == null) { |
| | | if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) { |
| | | News.taskInfo(context.wrkMast().getWrkNo(), |
| | | "输送站点堵塞重规划未找到可下发路线,当前站点={},目标站点={}", |
| | | currentStationId, |
| | | targetStationId); |
| | | } else if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) { |
| | | News.taskInfo(context.wrkMast().getWrkNo(), |
| | | "站点任务停留超时后重算路径失败,当前站点={},目标站点={}", |
| | | currentStationId, |
| | | targetStationId); |
| | | } else { |
| | | News.taskInfo(context.wrkMast().getWrkNo(), "获取输送线命令失败"); |
| | | } |
| | | return RerouteCommandPlan.skip("missing-command"); |
| | | } |
| | | return RerouteCommandPlan.dispatch(command, decision, context.dispatchScene()); |
| | | } |
| | | |
| | | RerouteExecutionResult executeReroutePlan(RerouteContext context, |
| | | RerouteCommandPlan plan) { |
| | | if (context == null) { |
| | | return RerouteExecutionResult.skip("missing-context"); |
| | | } |
| | | if (plan == null) { |
| | | return RerouteExecutionResult.skip("missing-plan"); |
| | | } |
| | | if (plan.skip()) { |
| | | return RerouteExecutionResult.skip(plan.skipReason()); |
| | | } |
| | | StationProtocol stationProtocol = context.stationProtocol(); |
| | | if (stationProtocol == null) { |
| | | return RerouteExecutionResult.skip("missing-station-protocol"); |
| | | } |
| | | Integer taskNo = stationProtocol.getTaskNo(); |
| | | Integer stationId = stationProtocol.getStationId(); |
| | | if (taskNo == null || taskNo <= 0 || stationId == null) { |
| | | return RerouteExecutionResult.skip("invalid-station-task"); |
| | | } |
| | | boolean runBlockReroute = context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE; |
| | | if (runBlockReroute) { |
| | | // 站点进入堵塞后,设备侧可能已经把之前预下发的分段命令清掉了。 |
| | | // 先作废本地 session/segment 状态,再按新路线重发,避免被旧状态反向卡住。 |
| | | if (context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.cancelSession(taskNo); |
| | | } |
| | | if (context.resetSegmentCommandsBeforeDispatch()) { |
| | | resetSegmentMoveCommandsBeforeReroute(taskNo); |
| | | } |
| | | } |
| | | if (context.checkRecentDispatch() |
| | | && shouldSkipIdleRecoverForRecentDispatch(taskNo, stationId)) { |
| | | return RerouteExecutionResult.skip("recent-dispatch"); |
| | | } |
| | | int currentTaskBufferCommandCount = countCurrentTaskBufferCommands(stationProtocol.getTaskBufferItems(), taskNo); |
| | | if (currentTaskBufferCommandCount > 0 && !runBlockReroute) { |
| | | if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) { |
| | | News.info("输送站点任务停留超时,但缓存区仍存在当前任务命令,已跳过重算。站点号={},工作号={},当前任务命令数={}", |
| | | stationId, |
| | | taskNo, |
| | | currentTaskBufferCommandCount); |
| | | } |
| | | return RerouteExecutionResult.skip("buffer-has-current-task"); |
| | | } |
| | | if (currentTaskBufferCommandCount > 0 && runBlockReroute) { |
| | | // 堵塞重规划要替换的正是这些旧分段命令,不能再把残留 buffer 当成新的拦截条件。 |
| | | News.info("输送站点运行堵塞重规划检测到旧分段命令残留,已先清理本地状态后继续重发。站点号={},工作号={},当前任务命令数={}", |
| | | stationId, |
| | | taskNo, |
| | | currentTaskBufferCommandCount); |
| | | } |
| | | if (!runBlockReroute |
| | | && context.checkSuppressDispatch() |
| | | && stationMoveCoordinator != null |
| | | && stationMoveCoordinator.shouldSuppressDispatch(taskNo, stationId, plan.command())) { |
| | | return RerouteExecutionResult.skip("dispatch-suppressed"); |
| | | } |
| | | // 进入堵塞重规划后,旧路线已经被显式取消,本轮命令不再参与 active-session suppress 判定。 |
| | | if (context.requireOutOrderDispatchLock() |
| | | && !tryAcquireOutOrderDispatchLock(taskNo, stationId)) { |
| | | return RerouteExecutionResult.skip("out-order-lock"); |
| | | } |
| | | |
| | | if (!runBlockReroute |
| | | && context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.cancelSession(taskNo); |
| | | } |
| | | if (!isBlank(context.executionLockKey())) { |
| | | Object lock = redisUtil.get(context.executionLockKey()); |
| | | if (lock != null) { |
| | | return RerouteExecutionResult.skip("scene-lock"); |
| | | } |
| | | redisUtil.set(context.executionLockKey(), "lock", context.executionLockSeconds()); |
| | | } |
| | | if (!runBlockReroute && context.resetSegmentCommandsBeforeDispatch()) { |
| | | resetSegmentMoveCommandsBeforeReroute(taskNo); |
| | | } |
| | | |
| | | int clearedCommandCount = 0; |
| | | if (context.clearIdleIssuedCommands()) { |
| | | clearedCommandCount = clearIssuedMoveCommandsDuringIdleStay(context.idleTrack(), taskNo, stationId); |
| | | } |
| | | |
| | | boolean offered = offerDevpCommandWithDedup(context.dispatchDeviceNo(), plan.command(), plan.dispatchScene()); |
| | | if (!offered) { |
| | | return RerouteExecutionResult.skip("dispatch-dedup"); |
| | | } |
| | | |
| | | applyRerouteDispatchEffects(context, plan, clearedCommandCount); |
| | | return RerouteExecutionResult.dispatched(plan.command(), clearedCommandCount); |
| | | } |
| | | |
| | | RerouteDecision resolveSharedRerouteDecision(RerouteContext context) { |
| | | if (context == null || context.wrkMast() == null || context.stationProtocol() == null) { |
| | | return RerouteDecision.skip("missing-runtime-dependency"); |
| | | } |
| | | Integer currentStationId = context.stationProtocol().getStationId(); |
| | | if (currentStationId == null) { |
| | | return RerouteDecision.skip("missing-current-station"); |
| | | } |
| | | |
| | | if (context.sceneType() == RerouteSceneType.IDLE_RECOVER |
| | | && !Objects.equals(context.wrkMast().getWrkSts(), WrkStsType.STATION_RUN.sts)) { |
| | | Integer targetStationId = context.wrkMast().getStaNo(); |
| | | return targetStationId == null || Objects.equals(targetStationId, currentStationId) |
| | | ? RerouteDecision.skip("same-station") |
| | | : RerouteDecision.proceed(targetStationId); |
| | | } |
| | | |
| | | OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision( |
| | | currentStationId, |
| | | context.wrkMast(), |
| | | context.outOrderStationIds(), |
| | | context.pathLenFactor() |
| | | ); |
| | | Integer targetStationId = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | if (targetStationId == null || Objects.equals(targetStationId, currentStationId)) { |
| | | return RerouteDecision.skip("same-station"); |
| | | } |
| | | return RerouteDecision.proceed(targetStationId, dispatchDecision); |
| | | } |
| | | |
| | | private RerouteExecutionResult executeSharedReroute(RerouteContext context) { |
| | | RerouteDecision decision = resolveSharedRerouteDecision(context); |
| | | if (decision.skip()) { |
| | | return RerouteExecutionResult.skip(decision.skipReason()); |
| | | } |
| | | RerouteCommandPlan plan = buildRerouteCommandPlan(context, decision); |
| | | return executeReroutePlan(context, plan); |
| | | } |
| | | |
| | | private void applyRerouteDispatchEffects(RerouteContext context, |
| | | RerouteCommandPlan plan, |
| | | int clearedCommandCount) { |
| | | if (context == null || plan == null || plan.command() == null || context.wrkMast() == null || context.stationProtocol() == null) { |
| | | return; |
| | | } |
| | | WrkMast wrkMast = context.wrkMast(); |
| | | StationProtocol stationProtocol = context.stationProtocol(); |
| | | OutOrderDispatchDecision dispatchDecision = plan.decision() == null ? null : plan.decision().dispatchDecision(); |
| | | |
| | | syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), context.outOrderStationIds(), dispatchDecision, plan.command()); |
| | | if (stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.recordDispatch( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | plan.dispatchScene(), |
| | | plan.command(), |
| | | dispatchDecision != null && dispatchDecision.isCircle() |
| | | ); |
| | | } |
| | | if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) { |
| | | saveStationTaskIdleTrack(new StationTaskIdleTrack(wrkMast.getWrkNo(), stationProtocol.getStationId(), System.currentTimeMillis())); |
| | | News.info("输送站点任务停留{}秒未运行,已重新计算路径并重启运行,站点号={},目标站={},工作号={},清理旧分段命令数={},命令数据={}", |
| | | STATION_IDLE_RECOVER_SECONDS, |
| | | stationProtocol.getStationId(), |
| | | plan.command().getTargetStaNo(), |
| | | wrkMast.getWrkNo(), |
| | | clearedCommandCount, |
| | | JSON.toJSONString(plan.command())); |
| | | return; |
| | | } |
| | | if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) { |
| | | News.info("输送站点堵塞后重新计算路径命令下发成功,站点号={},工作号={},命令数据={}", |
| | | stationProtocol.getStationId(), |
| | | wrkMast.getWrkNo(), |
| | | JSON.toJSONString(plan.command())); |
| | | return; |
| | | } |
| | | if (context.sceneType() == RerouteSceneType.OUT_ORDER) { |
| | | News.info(dispatchDecision != null && dispatchDecision.isCircle() ? "{}任务进行绕圈" : "{}任务直接去目标点", wrkMast.getWrkNo()); |
| | | } |
| | | } |
| | | |
| | | private List<NavigateNode> calcOutboundNavigatePath(WrkMast wrkMast, |
| | | Integer sourceStationId, |
| | | Integer targetStationId, |
| | | Double pathLenFactor) { |
| | | Double normalizedFactor = normalizePathLenFactor(pathLenFactor); |
| | | Integer currentTaskNo = wrkMast == null ? null : wrkMast.getWrkNo(); |
| | | if (currentTaskNo == null) { |
| | | return navigateUtils.calcByStationId(sourceStationId, targetStationId, normalizedFactor); |
| | | } |
| | | return navigateUtils.calcByStationId(sourceStationId, targetStationId, currentTaskNo, normalizedFactor); |
| | | } |
| | | |
| | | /** |
| | | * 计算当前出库任务的路径倾向系数。 |
| | | * |
| | | * <p>这个系数不是业务目标站本身,而是“在多条可行路线之间更偏向哪一条”的辅助输入, |
| | | * 目的是让同一批次、不同序号的任务在共享环线里尽量形成稳定、可重复的路径分布。 |
| | | * |
| | | * <p>返回值范围固定在 {@code [0, 1]}: |
| | | * 1. 非批次出库任务,直接返回 {@code 0.0},表示不引入额外路径偏置。 |
| | | * 2. 当前批次只有 1 个有效活动任务,返回 {@code 0.0},因为没有“前后顺序”可比较。 |
| | | * 3. 否则按“当前任务前面还有多少个有效前序任务”占“有效活动任务总数”的比例来算。 |
| | | * |
| | | * <p>这里的“有效任务”只统计: |
| | | * 1. ioType=OUT 的出库任务; |
| | | * 2. 仍处于活动状态,未完成/未结算; |
| | | * 3. 有 batchSeq; |
| | | * 4. mk != taskCancel。 |
| | | * |
| | | * <p>结果含义可以直观理解为: |
| | | * 任务批次序号越靠后,前面已经存在的有效任务越多,得到的系数越大; |
| | | * 后续算路时就更容易和前序任务形成稳定的路径分流,而不是所有任务都走同一条默认短路。 |
| | | * |
| | | * <p>注意: |
| | | * 这个方法不直接决定目标站,不负责排序放行,只提供“路径偏好”输入。 |
| | | * 真正的目标站仍由 {@link #resolveOutboundDispatchDecision(Integer, WrkMast, List, Double)} 决定。 |
| | | */ |
| | | private Double resolveOutboundPathLenFactor(WrkMast wrkMast) { |
| | | if (!isBatchOutboundTaskWithSeq(wrkMast)) { |
| | | return 0.0d; |
| | | } |
| | | List<WrkMast> activeBatchTaskList = loadActiveBatchTaskList(wrkMast.getBatch()); |
| | | if (activeBatchTaskList.size() <= 1) { |
| | | return 0.0d; |
| | | } |
| | | |
| | | int activeTaskCount = 0; |
| | | int predecessorCount = 0; |
| | | for (WrkMast item : activeBatchTaskList) { |
| | | if (!isFactorCandidateTask(item)) { |
| | | continue; |
| | | } |
| | | activeTaskCount++; |
| | | if (item.getBatchSeq() < wrkMast.getBatchSeq()) { |
| | | predecessorCount++; |
| | | } |
| | | } |
| | | if (activeTaskCount <= 1 || predecessorCount <= 0) { |
| | | return 0.0d; |
| | | } |
| | | return normalizePathLenFactor((double) predecessorCount / (double) (activeTaskCount - 1)); |
| | | } |
| | | |
| | | /** |
| | | * 判断当前任务是否具备“按批次出库规则参与排序/路径偏好计算”的基础条件。 |
| | | * |
| | | * <p>这里只做最基础的资格过滤,不关心当前是否真的需要排序点介入。 |
| | | * 只要不是批次出库任务,后面的路径偏好系数与排序目标决策都应该直接退化为默认行为。 |
| | | */ |
| | | private boolean isBatchOutboundTaskWithSeq(WrkMast wrkMast) { |
| | | return wrkMast != null |
| | | && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id) |
| | | && !Cools.isEmpty(wrkMast.getBatch()) |
| | | && wrkMast.getBatchSeq() != null |
| | | && wrkMast.getWrkNo() != null; |
| | | } |
| | | |
| | | /** |
| | | * 加载同一批次下仍处于活动中的出库任务。 |
| | | * |
| | | * <p>这里用于两类计算: |
| | | * 1. 计算路径偏好系数时,统计当前任务前面还有多少个有效前序任务。 |
| | | * 2. 当前排序点重新决策时,找出这一批“首个未完成任务”的实际批次序号。 |
| | | * |
| | | * <p>已经完成/结算的任务不再参与当前批次的排序与偏好计算。 |
| | | */ |
| | | private List<WrkMast> loadActiveBatchTaskList(String batch) { |
| | | if (Cools.isEmpty(batch)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return wrkMastService.list(new QueryWrapper<WrkMast>() |
| | | .eq("io_type", WrkIoType.OUT.id) |
| | | .eq("batch", batch) |
| | | .notIn("wrk_sts", |
| | | WrkStsType.STATION_RUN_COMPLETE.sts, |
| | | WrkStsType.COMPLETE_OUTBOUND.sts, |
| | | WrkStsType.SETTLE_OUTBOUND.sts)); |
| | | } |
| | | |
| | | /** |
| | | * 判断某条批次任务是否应该计入路径偏好系数的分母/分子统计。 |
| | | * |
| | | * <p>这里排除没有 batchSeq 的任务以及被显式标记为 taskCancel 的任务, |
| | | * 避免无效任务把同批次的路径偏好计算拉偏。 |
| | | */ |
| | | private boolean isFactorCandidateTask(WrkMast wrkMast) { |
| | | return wrkMast != null |
| | | && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id) |
| | | && wrkMast.getBatchSeq() != null |
| | | && !"taskCancel".equals(wrkMast.getMk()); |
| | | } |
| | | |
| | | public List<Integer> getAllOutOrderList() { |
| | |
| | | return list; |
| | | } |
| | | |
| | | /** |
| | | * 统一计算当前任务“此刻应该朝哪个目标站继续运行”。 |
| | | * |
| | | * <p>这是出库排序、绕圈、堵塞重规划共用的目标裁决入口。 |
| | | * 不管触发来源是 OUT_ORDER、WATCH_CIRCLE 还是 RUN_BLOCK_REROUTE, |
| | | * 只要业务语义还是“当前这票出库任务下一步该往哪里走”,都从这里得出目标站。 |
| | | * |
| | | * <p>它做三层判断: |
| | | * 1. 如果当前任务根本不适用出库排序,直接返回任务业务目标站 {@code wrkMast.staNo}。 |
| | | * 2. 如果适用出库排序,先算出“当前批次规则下,此刻允许前往的 dispatchStationId”。 |
| | | * 3. 如果当前站点正好就是排序决策点,再进一步判断是: |
| | | * 直接去目标点,还是先进入绕圈目标点,或者因为严格窗口限制而暂不放行。 |
| | | * |
| | | * <p>返回的 {@link OutOrderDispatchDecision} 不只是一个目标站, |
| | | * 还携带了这次决策是否属于绕圈、是否来自当前排序点重新裁决等语义信息, |
| | | * 供后续日志、session 记录和 watch-circle 判定使用。 |
| | | * |
| | | * <p>参数含义: |
| | | * 1. {@code currentStationId}:任务当前所在站点。用于判断当前是不是排序决策点、 |
| | | * 当前是不是已经到达 watch-circle 的下一决策站。 |
| | | * 2. {@code wrkMast}:当前任务主状态,至少要提供业务目标站、来源站、批次、序号等信息。 |
| | | * 3. {@code outOrderStationIds}:当前设备配置的所有出库排序站点列表。 |
| | | * 4. {@code pathLenFactor}:由 {@link #resolveOutboundPathLenFactor(WrkMast)} 得到的路径偏好系数, |
| | | * 用来让同一批次任务在选择 dispatch target 时保持稳定的路径倾向。 |
| | | * |
| | | * <p>返回值语义: |
| | | * 1. 返回 {@code null}:当前无法得到合法目标站,调用方应跳过本次派发。 |
| | | * 2. 返回 {@code targetStationId=wrkMast.staNo, circle=false}: |
| | | * 当前不需要出库排序干预,或已允许直接去业务目标站。 |
| | | * 3. 返回 {@code targetStationId!=wrkMast.staNo}: |
| | | * 当前应该先去一个中间 dispatch 目标站,后续再由排序点/绕圈点继续决策。 |
| | | * 4. 返回 {@code circle=true}: |
| | | * 当前属于绕圈决策结果,后续 watch-circle 逻辑会据此接管。 |
| | | * |
| | | * <p>注意: |
| | | * 这个方法只决定“目标站”,不直接生成输送命令。 |
| | | * 真正的路径由普通算路或 run-block 专用算路在后续步骤生成。 |
| | | */ |
| | | private OutOrderDispatchDecision resolveOutboundDispatchDecision(Integer currentStationId, |
| | | WrkMast wrkMast, |
| | | List<Integer> outOrderStationIds) { |
| | | List<Integer> outOrderStationIds, |
| | | Double pathLenFactor) { |
| | | if (wrkMast == null || wrkMast.getStaNo() == null) { |
| | | return null; |
| | | } |
| | |
| | | return new OutOrderDispatchDecision(wrkMast.getStaNo(), false); |
| | | } |
| | | Integer dispatchStationId = resolveDispatchOutOrderTarget( |
| | | wrkMast, |
| | | wrkMast.getSourceStaNo(), |
| | | wrkMast.getStaNo(), |
| | | outOrderStationIds |
| | | outOrderStationIds, |
| | | pathLenFactor |
| | | ); |
| | | if (dispatchStationId == null) { |
| | | return null; |
| | | } |
| | | if (isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds)) { |
| | | return resolveCurrentOutOrderDispatchDecision(currentStationId, wrkMast, outOrderStationIds); |
| | | if (isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) { |
| | | return resolveCurrentOutOrderDispatchDecision(currentStationId, wrkMast, outOrderStationIds, pathLenFactor); |
| | | } |
| | | if (!Objects.equals(dispatchStationId, wrkMast.getStaNo()) |
| | | && isCurrentOutOrderStation(currentStationId, outOrderStationIds) |
| | | && isWatchingCircleArrival(wrkMast.getWrkNo(), currentStationId)) { |
| | | return new OutOrderDispatchDecision(dispatchStationId, true); |
| | | return new OutOrderDispatchDecision(dispatchStationId, true, null, false); |
| | | } |
| | | return new OutOrderDispatchDecision(dispatchStationId, false); |
| | | } |
| | | |
| | | /** |
| | | * 在“当前站点就是本次排序决策点”时,计算这里到底该直接放行还是进入绕圈。 |
| | | * |
| | | * <p>这是出库排序里最核心的局部决策方法。进入这里之前,已经满足: |
| | | * 1. 当前任务适用 out-order; |
| | | * 2. 当前站点就是这票任务此刻对应的 dispatch 排序点。 |
| | | * |
| | | * <p>内部决策顺序: |
| | | * 1. 先取出同批次仍未完成的任务,找出这批当前“应当被优先放行”的序号位置。 |
| | | * 2. 再结合当前任务在初始路径上的排序窗口位置,判断自己此刻能否直接去业务目标站。 |
| | | * 3. 如果理论上该自己放行,还要额外检查目标方向上是否存在可进入的 release slot。 |
| | | * 4. 如果不能直达,或者直达方向当前全部阻塞,就转成 circle 决策,寻找下一排序检测点。 |
| | | * |
| | | * <p>返回值: |
| | | * 1. {@code circle=false} 表示当前排序点已经允许直接去业务目标站。 |
| | | * 2. {@code circle=true} 表示当前只能先去下一绕圈目标站,后续由 watch-circle/排序点继续接力决策。 |
| | | * 3. {@code null} 表示当前既不能直达,也没找到合法的下一绕圈点,调用方应跳过本次派发。 |
| | | */ |
| | | private OutOrderDispatchDecision resolveCurrentOutOrderDispatchDecision(Integer currentStationId, |
| | | WrkMast wrkMast, |
| | | List<Integer> outOrderStationIds) { |
| | | if (!isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds)) { |
| | | List<Integer> outOrderStationIds, |
| | | Double pathLenFactor) { |
| | | if (!isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) { |
| | | return null; |
| | | } |
| | | |
| | |
| | | |
| | | List<NavigateNode> initPath; |
| | | try { |
| | | initPath = navigateUtils.calcByStationId(wrkMast.getSourceStaNo(), wrkMast.getStaNo()); |
| | | initPath = calcOutboundNavigatePath(wrkMast, wrkMast.getSourceStaNo(), wrkMast.getStaNo(), pathLenFactor); |
| | | } catch (Exception e) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "批次:{} 计算排序路径失败,当前站点={}", wrkMast.getBatch(), currentStationId); |
| | | return null; |
| | |
| | | toTarget = Integer.valueOf(seq + 1).equals(wrkMast.getBatchSeq()); |
| | | } |
| | | if (toTarget) { |
| | | if (hasReachableOutReleaseSlot(currentStationId, wrkMast.getStaNo())) { |
| | | if (hasReachableOutReleaseSlot(wrkMast, currentStationId, wrkMast.getStaNo(), pathLenFactor)) { |
| | | return new OutOrderDispatchDecision(wrkMast.getStaNo(), false); |
| | | } |
| | | Integer circleTarget = resolveNextCircleOrderTarget(currentStationId, outOrderStationIds); |
| | | StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop( |
| | | wrkMast.getWrkNo(), |
| | | currentStationId, |
| | | outOrderStationIds |
| | | ); |
| | | Integer circleTarget = resolveNextCircleOrderTarget( |
| | | wrkMast, |
| | | currentStationId, |
| | | outOrderStationIds, |
| | | loopEvaluation.getExpectedLoopIssueCount(), |
| | | pathLenFactor |
| | | ); |
| | | if (circleTarget == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "目标站当前不可进,且未找到可执行的下一排序检测点,当前站点={}", currentStationId); |
| | | return null; |
| | | } |
| | | return new OutOrderDispatchDecision(circleTarget, true); |
| | | return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true); |
| | | } |
| | | |
| | | Integer circleTarget = resolveNextCircleOrderTarget(currentStationId, outOrderStationIds); |
| | | StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop( |
| | | wrkMast.getWrkNo(), |
| | | currentStationId, |
| | | outOrderStationIds |
| | | ); |
| | | Integer circleTarget = resolveNextCircleOrderTarget( |
| | | wrkMast, |
| | | currentStationId, |
| | | outOrderStationIds, |
| | | loopEvaluation.getExpectedLoopIssueCount(), |
| | | pathLenFactor |
| | | ); |
| | | if (circleTarget == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "未找到可执行的下一排序检测点,当前站点={}", currentStationId); |
| | | return null; |
| | | } |
| | | return new OutOrderDispatchDecision(circleTarget, true); |
| | | return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true); |
| | | } |
| | | |
| | | /** |
| | | * 判断这票任务在当前设备上是否应该启用出库排序逻辑。 |
| | | * |
| | | * <p>只要缺少任何一个排序前提,例如不是出库任务、没有批次、没有序号、 |
| | | * 当前设备也没有配置排序点,就应该直接退回“普通目标站决策”。 |
| | | */ |
| | | private boolean shouldApplyOutOrder(WrkMast wrkMast, List<Integer> outOrderStationIds) { |
| | | return wrkMast != null |
| | | && wrkMast.getStaNo() != null |
| | |
| | | && !outOrderStationIds.isEmpty(); |
| | | } |
| | | |
| | | /** |
| | | * 判断当前所在站点是否就是“这票任务此刻应该触发排序决策的 dispatch 站点”。 |
| | | * |
| | | * <p>注意它不是简单判断“当前站点是否属于排序点列表”, |
| | | * 而是先根据完整路径反推出当前任务对应的 dispatch 排序点, |
| | | * 再判断当前位置是否正好等于这个 dispatch 点。 |
| | | */ |
| | | private boolean isCurrentOutOrderDispatchStation(Integer currentStationId, |
| | | WrkMast wrkMast, |
| | | List<Integer> outOrderStationIds) { |
| | | List<Integer> outOrderStationIds, |
| | | Double pathLenFactor) { |
| | | if (!shouldApplyOutOrder(wrkMast, outOrderStationIds) || currentStationId == null) { |
| | | return false; |
| | | } |
| | | Integer dispatchStationId = resolveDispatchOutOrderTarget( |
| | | wrkMast, |
| | | wrkMast.getSourceStaNo(), |
| | | wrkMast.getStaNo(), |
| | | outOrderStationIds |
| | | outOrderStationIds, |
| | | pathLenFactor |
| | | ); |
| | | return dispatchStationId != null |
| | | && !Objects.equals(dispatchStationId, wrkMast.getStaNo()) |
| | | && Objects.equals(currentStationId, dispatchStationId); |
| | | } |
| | | |
| | | /** |
| | | * 判断当前位置是否属于设备配置里的任意一个排序点。 |
| | | * |
| | | * <p>这个判断比 {@link #isCurrentOutOrderDispatchStation(Integer, WrkMast, List, Double)} 更宽, |
| | | * 只回答“当前位置是不是排序点”,不回答“是不是这票任务当前应该命中的排序点”。 |
| | | */ |
| | | private boolean isCurrentOutOrderStation(Integer currentStationId, |
| | | List<Integer> outOrderStationIds) { |
| | | return currentStationId != null |
| | |
| | | } |
| | | if (dispatchDecision.isCircle()) { |
| | | saveWatchCircleCommand(wrkMast.getWrkNo(), command); |
| | | if (dispatchDecision.shouldCountLoopIssue() |
| | | && stationTaskLoopService != null |
| | | && dispatchDecision.getLoopEvaluation() != null) { |
| | | stationTaskLoopService.recordLoopIssue(dispatchDecision.getLoopEvaluation(), "OUT_ORDER_CIRCLE"); |
| | | } |
| | | } else { |
| | | clearWatchCircleCommand(wrkMast.getWrkNo()); |
| | | } |
| | | } |
| | | |
| | | private Integer resolveDispatchOutOrderTarget(Integer sourceStationId, |
| | | /** |
| | | * 为当前排序点决策预先做一次环线评估。 |
| | | * |
| | | * <p>当本次决策最终进入绕圈时,评估结果会被带进 {@link OutOrderDispatchDecision}, |
| | | * 后续用于记录 loop issue 统计,而不是在真正下发后再重复评估一次。 |
| | | */ |
| | | private StationTaskLoopService.LoopEvaluation evaluateOutOrderLoop(Integer taskNo, |
| | | Integer currentStationId, |
| | | List<Integer> outOrderStationIds) { |
| | | if (stationTaskLoopService == null) { |
| | | return new StationTaskLoopService.LoopEvaluation( |
| | | taskNo, |
| | | currentStationId, |
| | | StationTaskLoopService.LoopIdentitySnapshot.empty(), |
| | | 0, |
| | | 0, |
| | | false |
| | | ); |
| | | } |
| | | return stationTaskLoopService.evaluateLoop( |
| | | taskNo, |
| | | currentStationId, |
| | | true, |
| | | outOrderStationIds, |
| | | "outOrderCircle" |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 从“源站到业务目标站”的完整路径里,反推出当前任务应当先命中的 dispatch 排序点。 |
| | | * |
| | | * <p>做法是: |
| | | * 1. 先计算从 sourceStationId 到 finalTargetStationId 的完整导航路径; |
| | | * 2. 再从路径尾部向前扫描; |
| | | * 3. 找到离最终目标最近的那个排序点,作为当前 dispatch 目标。 |
| | | * |
| | | * <p>如果路径上根本没有排序点,或者缺少源站/排序点配置, |
| | | * 就直接把业务目标站本身当成 dispatch 目标。 |
| | | */ |
| | | private Integer resolveDispatchOutOrderTarget(WrkMast wrkMast, |
| | | Integer sourceStationId, |
| | | Integer finalTargetStationId, |
| | | List<Integer> outOrderList) { |
| | | List<Integer> outOrderList, |
| | | Double pathLenFactor) { |
| | | if (finalTargetStationId == null) { |
| | | return null; |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | List<NavigateNode> nodes = navigateUtils.calcByStationId(sourceStationId, finalTargetStationId); |
| | | List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, sourceStationId, finalTargetStationId, pathLenFactor); |
| | | for (int i = nodes.size() - 1; i >= 0; i--) { |
| | | Integer stationId = getStationIdFromNode(nodes.get(i)); |
| | | if (stationId == null) { |
| | |
| | | return finalTargetStationId; |
| | | } |
| | | |
| | | private boolean hasReachableOutReleaseSlot(Integer currentStationId, |
| | | Integer finalTargetStationId) { |
| | | /** |
| | | * 判断从当前排序点继续前往最终业务目标站时,路径上是否至少存在一个可进入的后续站点。 |
| | | * |
| | | * <p>这里不是要求整条路径完全畅通,而是判断“当前是否有释放口可走”。 |
| | | * 只要后续路径上存在一个未阻塞站点,就认为当前仍可尝试直达; |
| | | * 只有整段后续路径看起来都被阻塞时,才会强制转入绕圈。 |
| | | */ |
| | | private boolean hasReachableOutReleaseSlot(WrkMast wrkMast, |
| | | Integer currentStationId, |
| | | Integer finalTargetStationId, |
| | | Double pathLenFactor) { |
| | | if (currentStationId == null || finalTargetStationId == null) { |
| | | return true; |
| | | } |
| | | |
| | | try { |
| | | List<NavigateNode> nodes = navigateUtils.calcByStationId(currentStationId, finalTargetStationId); |
| | | List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, currentStationId, finalTargetStationId, pathLenFactor); |
| | | if (nodes == null || nodes.isEmpty()) { |
| | | return true; |
| | | } |
| | |
| | | || (stationProtocol.getTaskNo() != null && stationProtocol.getTaskNo() > 0); |
| | | } |
| | | |
| | | private Integer resolveNextCircleOrderTarget(Integer currentStationId, List<Integer> orderedOutStationList) { |
| | | /** |
| | | * 为“排序点需要绕圈”的场景,从出库排序站点列表里挑出下一跳绕圈目标。 |
| | | * <p> |
| | | * 这里的输入不是整张地图,而是已经按业务顺序排好的出库站点序列。方法会从当前站点在该序列中的后继开始, |
| | | * 依次尝试每个候选站点,并计算“当前站点 -> 候选站点”的可行路径: |
| | | * <p> |
| | | * 1. 只要能算出路径,就把候选站点记录为一个可选绕圈目标; |
| | | * 2. 记录两类排序信息: |
| | | * - pathLength:到该候选点的路径长度,越短说明越适合先拿来做临时缓冲; |
| | | * - offset:该站点在排序序列中距离当前站点有多远,用来在路径长度相同的时候保留既有顺序感; |
| | | * 3. 最后把候选列表交给 {@link #resolveGradualCircleTargetByPathLength(Integer, List, Double)}, |
| | | * 再根据“当前已经绕圈多少次”与“路径长度偏好系数”从不同长度层级里挑最终目标。 |
| | | * <p> |
| | | * 这个方法本身不判断“是否应该绕圈”,只负责在已经决定绕圈后,从排序站点链路里找一个下一跳缓冲点。 |
| | | * |
| | | * @param wrkMast 当前任务,主要用于算路 |
| | | * @param currentStationId 当前站点,即本次准备从哪里发出绕圈命令 |
| | | * @param orderedOutStationList 已按业务顺序排好的出库站点列表 |
| | | * @param expectedLoopIssueCount 预计已发生的绕圈/堵塞轮次,用于决定是否逐步放大绕圈半径 |
| | | * @param pathLenFactor 当前任务对应的路径偏好系数,影响 calcOutboundNavigatePath 的选路结果 |
| | | * @return 下一跳绕圈目标站;如果没有任何可到达候选则返回 null |
| | | */ |
| | | private Integer resolveNextCircleOrderTarget(WrkMast wrkMast, |
| | | Integer currentStationId, |
| | | List<Integer> orderedOutStationList, |
| | | Integer expectedLoopIssueCount, |
| | | Double pathLenFactor) { |
| | | if (currentStationId == null || orderedOutStationList == null || orderedOutStationList.size() <= 1) { |
| | | return null; |
| | | } |
| | | |
| | | int startIndex = orderedOutStationList.indexOf(currentStationId); |
| | | int total = orderedOutStationList.size(); |
| | | List<CircleTargetCandidate> candidateList = new ArrayList<>(); |
| | | for (int offset = 1; offset < total; offset++) { |
| | | int candidateIndex = (startIndex + offset + total) % total; |
| | | Integer candidateStationId = orderedOutStationList.get(candidateIndex); |
| | |
| | | continue; |
| | | } |
| | | try { |
| | | List<NavigateNode> path = navigateUtils.calcByStationId(currentStationId, candidateStationId); |
| | | List<NavigateNode> path = calcOutboundNavigatePath(wrkMast, currentStationId, candidateStationId, pathLenFactor); |
| | | if (path != null && !path.isEmpty()) { |
| | | return candidateStationId; |
| | | candidateList.add(new CircleTargetCandidate(candidateStationId, path.size(), offset)); |
| | | } |
| | | } catch (Exception ignore) {} |
| | | } |
| | | return null; |
| | | if (candidateList.isEmpty()) { |
| | | return null; |
| | | } |
| | | candidateList.sort(new Comparator<CircleTargetCandidate>() { |
| | | @Override |
| | | public int compare(CircleTargetCandidate left, CircleTargetCandidate right) { |
| | | if (left == right) { |
| | | return 0; |
| | | } |
| | | if (left == null) { |
| | | return 1; |
| | | } |
| | | if (right == null) { |
| | | return -1; |
| | | } |
| | | int pathCompare = Integer.compare(left.getPathLength(), right.getPathLength()); |
| | | if (pathCompare != 0) { |
| | | return pathCompare; |
| | | } |
| | | return Integer.compare(left.getOffset(), right.getOffset()); |
| | | } |
| | | }); |
| | | return resolveGradualCircleTargetByPathLength(expectedLoopIssueCount, candidateList, pathLenFactor); |
| | | } |
| | | |
| | | /** |
| | | * 在“已按路径长度升序排好”的绕圈候选列表中,按层级渐进地挑选目标站。 |
| | | * <p> |
| | | * candidateList 里可能存在多个候选点拥有相同的 pathLength。对绕圈决策来说, |
| | | * 同一长度层里的候选点都属于“同一绕圈半径”,真正需要控制的是: |
| | | * <p> |
| | | * 1. 初次/前几次堵塞时,优先选择最短可达层,尽量用最小绕行距离恢复流转; |
| | | * 2. 如果任务已经连续多次在同一区域绕圈,说明短半径候选大概率已经试过或恢复效果差, |
| | | * 就需要逐步放大到更远一层; |
| | | * 3. pathLenFactor 代表当前任务对“短路径/长路径”的偏好,允许在相同堵塞轮次下适度往更远层偏移。 |
| | | * <p> |
| | | * 因此这里先把 candidateList 压缩成“按 pathLength 去重后的 tierList”,每个 tier 只保留该长度层的首个候选。 |
| | | * 然后同时计算两个层级索引: |
| | | * <p> |
| | | * - defaultTierIndex:基于 expectedLoopIssueCount 的默认放大层级; |
| | | * - factorTierIndex:基于 pathLenFactor 的偏好层级; |
| | | * <p> |
| | | * 最终取两者较大值,含义是“至少满足当前堵塞轮次需要的放大半径,同时允许路径偏好把目标推向更远层级”。 |
| | | * |
| | | * @param expectedLoopIssueCount 预计已发生的绕圈/堵塞轮次 |
| | | * @param candidateList 已按 pathLength、offset 排序的候选列表 |
| | | * @param pathLenFactor 当前任务的路径偏好系数 |
| | | * @return 最终选中的绕圈目标站;若没有候选则返回 null |
| | | */ |
| | | private Integer resolveGradualCircleTargetByPathLength(Integer expectedLoopIssueCount, |
| | | List<CircleTargetCandidate> candidateList, |
| | | Double pathLenFactor) { |
| | | if (candidateList == null || candidateList.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | List<CircleTargetCandidate> tierList = new ArrayList<>(); |
| | | Integer lastPathLength = null; |
| | | for (CircleTargetCandidate candidate : candidateList) { |
| | | if (candidate == null) { |
| | | continue; |
| | | } |
| | | if (lastPathLength == null || !Objects.equals(lastPathLength, candidate.getPathLength())) { |
| | | tierList.add(candidate); |
| | | lastPathLength = candidate.getPathLength(); |
| | | } |
| | | } |
| | | if (tierList.isEmpty()) { |
| | | return candidateList.get(0).getStationId(); |
| | | } |
| | | int defaultTierIndex = expectedLoopIssueCount == null || expectedLoopIssueCount <= 2 |
| | | ? 0 |
| | | : Math.min(expectedLoopIssueCount - 2, tierList.size() - 1); |
| | | int factorTierIndex = (int) Math.round(normalizePathLenFactor(pathLenFactor) * (tierList.size() - 1)); |
| | | int tierIndex = Math.max(defaultTierIndex, factorTierIndex); |
| | | return tierList.get(tierIndex).getStationId(); |
| | | } |
| | | |
| | | private boolean tryAcquireOutOrderDispatchLock(Integer wrkNo, Integer stationId) { |
| | |
| | | return true; |
| | | } |
| | | |
| | | private boolean shouldSkipOutOrderDispatchForExistingRoute(Integer wrkNo, Integer stationId) { |
| | | if (stationMoveCoordinator == null || wrkNo == null || wrkNo <= 0 || stationId == null) { |
| | | return false; |
| | | } |
| | | StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo); |
| | | if (session == null) { |
| | | return false; |
| | | } |
| | | if (!session.isActive() || !session.containsStation(stationId)) { |
| | | return false; |
| | | } |
| | | // 绕圈路线在当前站点尚未走完时,排序点不应再次插手。 |
| | | if (StationMoveDispatchMode.CIRCLE == session.getDispatchMode()) { |
| | | return true; |
| | | } |
| | | // 直接路线只在“当前站点已经被别的活动路线占住且目标不同”时才拦截。 |
| | | return !Objects.equals(stationId, session.getCurrentRouteTargetStationId()); |
| | | } |
| | | |
| | | private boolean isWatchingCircleArrival(Integer wrkNo, Integer stationId) { |
| | | if (stationMoveCoordinator != null) { |
| | | StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo); |
| | | if (session != null && session.isActive() && stationId != null) { |
| | | // nextDecisionStationId 表示这条路线真正等待重新决策的站点,到站才触发 watch-circle。 |
| | | if (stationId.equals(session.getNextDecisionStationId())) { |
| | | return true; |
| | | } |
| | | // 还在 session 路径中间站运行时不应误触发。 |
| | | if (session.containsStation(stationId)) { |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | StationCommand command = getWatchCircleCommand(wrkNo); |
| | | return command != null && stationId != null && stationId.equals(command.getTargetStaNo()); |
| | | } |
| | | |
| | | private boolean isWatchingCircleTransit(Integer wrkNo, Integer stationId) { |
| | | if (stationMoveCoordinator != null) { |
| | | StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo); |
| | | if (session != null && session.isActive() && stationId != null) { |
| | | if (stationId.equals(session.getNextDecisionStationId())) { |
| | | return false; |
| | | } |
| | | if (session.containsStation(stationId)) { |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | StationCommand command = getWatchCircleCommand(wrkNo); |
| | | if (command == null || stationId == null || Objects.equals(stationId, command.getTargetStaNo())) { |
| | | return false; |
| | | } |
| | | List<Integer> navigatePath = command.getNavigatePath(); |
| | | return navigatePath != null && navigatePath.contains(stationId); |
| | | } |
| | | |
| | | private StationCommand getWatchCircleCommand(Integer wrkNo) { |
| | |
| | | } |
| | | |
| | | StationTaskIdleTrack idleTrack = touchStationTaskIdleTrack(stationProtocol.getTaskNo(), stationProtocol.getStationId()); |
| | | if (shouldSkipIdleRecoverForRecentDispatch(stationProtocol.getTaskNo(), stationProtocol.getStationId())) { |
| | | return; |
| | | } |
| | | if (idleTrack == null || !idleTrack.isTimeout(STATION_IDLE_RECOVER_SECONDS)) { |
| | | return; |
| | | } |
| | |
| | | if (lock != null) { |
| | | return; |
| | | } |
| | | Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast); |
| | | RerouteContext context = RerouteContext.create( |
| | | RerouteSceneType.IDLE_RECOVER, |
| | | basDevp, |
| | | stationThread, |
| | | stationProtocol, |
| | | wrkMast, |
| | | outOrderList, |
| | | pathLenFactor, |
| | | "checkStationIdleRecover" |
| | | ).withCancelSessionBeforeDispatch() |
| | | .withExecutionLock(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo(), STATION_IDLE_RECOVER_LIMIT_SECONDS) |
| | | .withResetSegmentCommandsBeforeDispatch() |
| | | .clearIdleIssuedCommands(idleTrack); |
| | | executeSharedReroute(context); |
| | | } |
| | | |
| | | OutOrderDispatchDecision dispatchDecision = null; |
| | | Integer moveStaNo; |
| | | if (Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts)) { |
| | | dispatchDecision = resolveOutboundDispatchDecision(stationProtocol.getStationId(), wrkMast, outOrderList); |
| | | moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId(); |
| | | } else { |
| | | moveStaNo = wrkMast.getStaNo(); |
| | | } |
| | | if (moveStaNo == null || Objects.equals(moveStaNo, stationProtocol.getStationId())) { |
| | | boolean shouldUseRunBlockDirectReassign(WrkMast wrkMast, |
| | | Integer stationId, |
| | | List<Integer> runBlockReassignLocStationList) { |
| | | return wrkMast != null |
| | | && Objects.equals(wrkMast.getIoType(), WrkIoType.IN.id) |
| | | && stationId != null |
| | | && runBlockReassignLocStationList != null |
| | | && runBlockReassignLocStationList.contains(stationId); |
| | | } |
| | | |
| | | private void executeRunBlockDirectReassign(BasDevp basDevp, |
| | | StationThread stationThread, |
| | | StationProtocol stationProtocol, |
| | | WrkMast wrkMast) { |
| | | if (basDevp == null || stationThread == null || stationProtocol == null || wrkMast == null) { |
| | | return; |
| | | } |
| | | |
| | | redisUtil.set(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo(), "lock", STATION_IDLE_RECOVER_LIMIT_SECONDS); |
| | | resetSegmentMoveCommandsBeforeReroute(stationProtocol.getTaskNo()); |
| | | int clearedCommandCount = clearIssuedMoveCommandsDuringIdleStay(idleTrack, stationProtocol.getTaskNo(), stationProtocol.getStationId()); |
| | | |
| | | StationCommand command = stationThread.getCommand( |
| | | StationCommandType.MOVE, |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | moveStaNo, |
| | | 0 |
| | | int currentTaskBufferCommandCount = countCurrentTaskBufferCommands( |
| | | stationProtocol.getTaskBufferItems(), |
| | | stationProtocol.getTaskNo() |
| | | ); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "站点任务停留超时后重算路径失败,当前站点={},目标站点={}", stationProtocol.getStationId(), moveStaNo); |
| | | if (currentTaskBufferCommandCount > 0) { |
| | | News.info("输送站点运行堵塞重分配已跳过,缓存区仍存在当前任务命令。站点号={},工作号={},当前任务命令数={}", |
| | | stationProtocol.getStationId(), |
| | | stationProtocol.getTaskNo(), |
| | | currentTaskBufferCommandCount); |
| | | return; |
| | | } |
| | | if (stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.cancelSession(wrkMast.getWrkNo()); |
| | | } |
| | | String response = wmsOperateUtils.applyReassignTaskLocNo(wrkMast.getWrkNo(), stationProtocol.getStationId()); |
| | | if (Cools.isEmpty(response)) { |
| | | News.taskError(wrkMast.getWrkNo(), "请求WMS重新分配库位接口失败,接口未响应!!!response:{}", response); |
| | | return; |
| | | } |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (!jsonObject.getInteger("code").equals(200)) { |
| | | News.error("请求WMS接口失败!!!response:{}", response); |
| | | return; |
| | | } |
| | | |
| | | MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command)); |
| | | syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), outOrderList, dispatchDecision, command); |
| | | saveStationTaskIdleTrack(new StationTaskIdleTrack(wrkMast.getWrkNo(), stationProtocol.getStationId(), System.currentTimeMillis())); |
| | | News.info("输送站点任务停留{}秒未运行,已重新计算路径并重启运行,站点号={},目标站={},工作号={},清理旧分段命令数={},命令数据={}", |
| | | STATION_IDLE_RECOVER_SECONDS, stationProtocol.getStationId(), moveStaNo, wrkMast.getWrkNo(), clearedCommandCount, JSON.toJSONString(command)); |
| | | StartupDto dto = jsonObject.getObject("data", StartupDto.class); |
| | | String sourceLocNo = wrkMast.getLocNo(); |
| | | String locNo = dto.getLocNo(); |
| | | |
| | | LocMast sourceLocMast = locMastService.queryByLoc(sourceLocNo); |
| | | if (sourceLocMast == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位信息不存在", sourceLocNo); |
| | | return; |
| | | } |
| | | if (!sourceLocMast.getLocSts().equals("S")) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位状态不处于入库预约", sourceLocNo); |
| | | return; |
| | | } |
| | | |
| | | LocMast locMast = locMastService.queryByLoc(locNo); |
| | | if (locMast == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位信息不存在", locNo); |
| | | return; |
| | | } |
| | | if (!locMast.getLocSts().equals("O")) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位状态不处于空库位", locNo); |
| | | return; |
| | | } |
| | | |
| | | FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo); |
| | | if (findCrnNoResult == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo()); |
| | | return; |
| | | } |
| | | Integer crnNo = findCrnNoResult.getCrnNo(); |
| | | |
| | | Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationProtocol.getStationId()); |
| | | if (targetStationId == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationProtocol.getStationId()); |
| | | return; |
| | | } |
| | | |
| | | StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), targetStationId, 0); |
| | | if (command == null) { |
| | | News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo()); |
| | | return; |
| | | } |
| | | |
| | | sourceLocMast.setLocSts("O"); |
| | | sourceLocMast.setModiTime(new Date()); |
| | | locMastService.updateById(sourceLocMast); |
| | | |
| | | locMast.setLocSts("S"); |
| | | locMast.setModiTime(new Date()); |
| | | locMastService.updateById(locMast); |
| | | |
| | | wrkMast.setLocNo(locNo); |
| | | wrkMast.setStaNo(targetStationId); |
| | | |
| | | if (findCrnNoResult.getCrnType().equals(SlaveType.Crn)) { |
| | | wrkMast.setCrnNo(crnNo); |
| | | } else if (findCrnNoResult.getCrnType().equals(SlaveType.DualCrn)) { |
| | | wrkMast.setDualCrnNo(crnNo); |
| | | } else { |
| | | throw new CoolException("未知设备类型"); |
| | | } |
| | | |
| | | if (!wrkMastService.updateById(wrkMast)) { |
| | | return; |
| | | } |
| | | boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "checkStationRunBlock_direct"); |
| | | if (!offered) { |
| | | return; |
| | | } |
| | | if (stationMoveCoordinator != null) { |
| | | stationMoveCoordinator.recordDispatch( |
| | | wrkMast.getWrkNo(), |
| | | stationProtocol.getStationId(), |
| | | "checkStationRunBlock_direct", |
| | | command, |
| | | false |
| | | ); |
| | | } |
| | | } |
| | | |
| | | private boolean canRecoverIdleStationTask(WrkMast wrkMast, Integer currentStationId) { |
| | |
| | | if (Objects.equals(currentStationId, wrkMast.getStaNo())) { |
| | | return false; |
| | | } |
| | | return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_DEVICE_RUN.sts) |
| | | return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts) |
| | | || Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts); |
| | | } |
| | | |
| | | private boolean shouldSkipIdleRecoverForRecentDispatch(Integer taskNo, Integer stationId) { |
| | | if (stationMoveCoordinator == null || taskNo == null || taskNo <= 0 || stationId == null) { |
| | | return false; |
| | | } |
| | | StationMoveSession session = stationMoveCoordinator.loadSession(taskNo); |
| | | if (session == null || !session.isActive() || session.getLastIssuedAt() == null) { |
| | | return false; |
| | | } |
| | | // 分段执行过程中,刚下发下一段命令时,session 的 currentStationId/dispatchStationId |
| | | // 可能还没来得及和当前观察站点完全对齐;只要当前站点仍在这条活动路线里, |
| | | // 就说明这次 recent dispatch 仍然和它相关,idle recover 不应在 10 秒窗口内再次介入。 |
| | | if (!Objects.equals(stationId, session.getCurrentStationId()) |
| | | && !Objects.equals(stationId, session.getDispatchStationId()) |
| | | && !session.containsStation(stationId)) { |
| | | return false; |
| | | } |
| | | long elapsedMs = System.currentTimeMillis() - session.getLastIssuedAt(); |
| | | long thresholdMs = STATION_IDLE_RECOVER_SECONDS * 1000L; |
| | | if (elapsedMs >= thresholdMs) { |
| | | return false; |
| | | } |
| | | saveStationTaskIdleTrack(new StationTaskIdleTrack(taskNo, stationId, System.currentTimeMillis())); |
| | | News.info("输送站点任务刚完成命令下发,已跳过停留重算。站点号={},工作号={},距上次下发={}ms,routeVersion={}", |
| | | stationId, taskNo, elapsedMs, session.getRouteVersion()); |
| | | return true; |
| | | } |
| | | |
| | | private void resetSegmentMoveCommandsBeforeReroute(Integer taskNo) { |
| | |
| | | } catch (Exception ignore) { |
| | | } |
| | | redisUtil.del(key); |
| | | } |
| | | |
| | | private int countCurrentTaskBufferCommands(List<StationTaskBufferItem> taskBufferItems, Integer currentTaskNo) { |
| | | if (taskBufferItems == null || taskBufferItems.isEmpty() || currentTaskNo == null || currentTaskNo <= 0) { |
| | | return 0; |
| | | } |
| | | int count = 0; |
| | | for (StationTaskBufferItem item : taskBufferItems) { |
| | | if (item == null || item.getTaskNo() == null) { |
| | | continue; |
| | | } |
| | | if (currentTaskNo.equals(item.getTaskNo())) { |
| | | count++; |
| | | } |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | private boolean offerDevpCommandWithDedup(Integer deviceNo, StationCommand command, String scene) { |
| | | if (deviceNo == null || command == null) { |
| | | return false; |
| | | } |
| | | String dedupKey = buildStationCommandDispatchDedupKey(deviceNo, command); |
| | | if (redisUtil != null) { |
| | | Object lock = redisUtil.get(dedupKey); |
| | | if (lock != null) { |
| | | News.info("输送站点命令短时重复派发,已跳过。scene={},deviceNo={},taskNo={},stationId={},targetStaNo={},commandType={}", |
| | | scene, |
| | | deviceNo, |
| | | command.getTaskNo(), |
| | | command.getStationId(), |
| | | command.getTargetStaNo(), |
| | | command.getCommandType()); |
| | | return false; |
| | | } |
| | | redisUtil.set(dedupKey, "lock", STATION_COMMAND_DISPATCH_DEDUP_SECONDS); |
| | | } |
| | | boolean offered = MessageQueue.offer(SlaveType.Devp, deviceNo, new Task(2, command)); |
| | | if (!offered && redisUtil != null) { |
| | | redisUtil.del(dedupKey); |
| | | } |
| | | return offered; |
| | | } |
| | | |
| | | private String buildStationCommandDispatchDedupKey(Integer deviceNo, StationCommand command) { |
| | | return RedisKeyType.STATION_COMMAND_DISPATCH_DEDUP_.key |
| | | + deviceNo + "_" |
| | | + command.getTaskNo() + "_" |
| | | + command.getStationId() + "_" |
| | | + (stationMoveCoordinator == null ? Integer.toHexString(buildFallbackPathSignature(command).hashCode()) |
| | | : stationMoveCoordinator.buildPathSignatureHash(command)); |
| | | } |
| | | |
| | | private String buildFallbackPathSignature(StationCommand command) { |
| | | if (command == null) { |
| | | return ""; |
| | | } |
| | | return String.valueOf(command.getCommandType()) |
| | | + "_" + command.getStationId() |
| | | + "_" + command.getTargetStaNo() |
| | | + "_" + command.getNavigatePath() |
| | | + "_" + command.getLiftTransferPath() |
| | | + "_" + command.getOriginalNavigatePath(); |
| | | } |
| | | |
| | | private int clearIssuedMoveCommandsDuringIdleStay(StationTaskIdleTrack idleTrack, |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 沿“源站 -> 目标站”的理论路径,从当前站点往下游回看,找出同批次任务在后续站点上的已知序号。 |
| | | * <p> |
| | | * 严格窗口控制要回答的问题不是“当前任务自己的 batchSeq 是多少”,而是: |
| | | * “在当前站点后面,沿这条出库链路已经排着的同批次任务,最靠近目标端的序号是多少?” |
| | | * 只有拿到这个序号,排序点才能判断当前任务是否应该紧跟其后放行。 |
| | | * <p> |
| | | * 具体做法: |
| | | * <p> |
| | | * 1. 按 pathList 从尾到头回扫,截出 searchStationId 之后的全部下游站点; |
| | | * 2. 读取这些站点当前正在执行的任务号; |
| | | * 3. 如果某站点上的任务属于 searchBatch,就记录它的 batchSeq; |
| | | * 4. 返回该批次在下游已知的序号。 |
| | | * <p> |
| | | * 这里返回的是“路径下游现场已出现的批次序号”,不是批次理论最小/最大值。 |
| | | * 如果下游没有同批次任务,返回 null,调用方需要退回到“当前批次首个未完成任务是否就是自己”的判定。 |
| | | * |
| | | * @param pathList 从任务源站到目标站的理论出库路径 |
| | | * @param searchStationId 当前正在做排序决策的站点 |
| | | * @param searchBatch 当前任务所属批次 |
| | | * @return 下游同批次任务的已知序号;若路径下游尚未出现该批次则返回 null |
| | | */ |
| | | public Integer getOutStationBatchSeq(List<NavigateNode> pathList, Integer searchStationId, String searchBatch) { |
| | | if (pathList == null || pathList.isEmpty() || searchStationId == null || Cools.isEmpty(searchBatch)) { |
| | | return null; |
| | | } |
| | | // 只关心当前站点之后的下游站点,当前站点之前的节点不会影响“谁应该排在我前面”。 |
| | | List<Integer> checkList = new ArrayList<>(); |
| | | for (int i = pathList.size() - 1; i >= 0; i--) { |
| | | NavigateNode node = pathList.get(i); |
| | |
| | | } |
| | | } |
| | | |
| | | // 下游站点可能同时挂着多个不同批次任务;这里只抽取与当前批次相关的现场序号快照。 |
| | | HashMap<String, Integer> batchMap = new HashMap<>(); |
| | | for (Integer station : checkList) { |
| | | BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", station)); |
| | |
| | | Integer sourceStationId, |
| | | Integer targetStationId, |
| | | LoadGuardState loadGuardState) { |
| | | return findPathLoopHit(config, sourceStationId, targetStationId, loadGuardState, null, null); |
| | | } |
| | | |
| | | private LoopHitResult findPathLoopHit(DispatchLimitConfig config, |
| | | Integer sourceStationId, |
| | | Integer targetStationId, |
| | | LoadGuardState loadGuardState, |
| | | WrkMast wrkMast, |
| | | Double pathLenFactor) { |
| | | if (!config.loopModeEnable) { |
| | | return LoopHitResult.NO_HIT; |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | List<NavigateNode> nodes = navigateUtils.calcByStationId(sourceStationId, targetStationId); |
| | | List<NavigateNode> nodes = wrkMast == null |
| | | ? navigateUtils.calcByStationId(sourceStationId, targetStationId) |
| | | : calcOutboundNavigatePath(wrkMast, sourceStationId, targetStationId, pathLenFactor); |
| | | if (nodes == null || nodes.isEmpty()) { |
| | | return LoopHitResult.NO_HIT; |
| | | } |
| | |
| | | return value; |
| | | } |
| | | |
| | | private Double normalizePathLenFactor(Double pathLenFactor) { |
| | | if (pathLenFactor == null || pathLenFactor < 0.0d) { |
| | | return 0.0d; |
| | | } |
| | | if (pathLenFactor > 1.0d) { |
| | | return 1.0d; |
| | | } |
| | | return pathLenFactor; |
| | | } |
| | | |
| | | enum RerouteSceneType { |
| | | RUN_BLOCK_REROUTE, |
| | | IDLE_RECOVER, |
| | | OUT_ORDER, |
| | | WATCH_CIRCLE |
| | | } |
| | | |
| | | static final class RerouteDecision { |
| | | private final boolean skip; |
| | | private final String skipReason; |
| | | private final Integer targetStationId; |
| | | private final OutOrderDispatchDecision dispatchDecision; |
| | | |
| | | private RerouteDecision(boolean skip, |
| | | String skipReason, |
| | | Integer targetStationId, |
| | | OutOrderDispatchDecision dispatchDecision) { |
| | | this.skip = skip; |
| | | this.skipReason = skipReason; |
| | | this.targetStationId = targetStationId; |
| | | this.dispatchDecision = dispatchDecision; |
| | | } |
| | | |
| | | static RerouteDecision skip(String reason) { |
| | | return new RerouteDecision(true, reason, null, null); |
| | | } |
| | | |
| | | static RerouteDecision proceed(Integer targetStationId) { |
| | | return new RerouteDecision(false, null, targetStationId, null); |
| | | } |
| | | |
| | | static RerouteDecision proceed(Integer targetStationId, |
| | | OutOrderDispatchDecision dispatchDecision) { |
| | | return new RerouteDecision(false, null, targetStationId, dispatchDecision); |
| | | } |
| | | |
| | | boolean skip() { |
| | | return skip; |
| | | } |
| | | |
| | | String skipReason() { |
| | | return skipReason; |
| | | } |
| | | |
| | | Integer targetStationId() { |
| | | return targetStationId; |
| | | } |
| | | |
| | | OutOrderDispatchDecision dispatchDecision() { |
| | | return dispatchDecision; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 重算/重发链路的一次性执行上下文。 |
| | | * |
| | | * <p>这个对象只在一次 reroute 执行过程中存在,不会落库,也不会长期缓存。 |
| | | * 它的职责不是表达业务状态,而是把“这次为什么进来、当前用哪套运行时对象、 |
| | | * 下发前后要不要启用额外控制逻辑”集中打包,供统一执行链路使用。 |
| | | * |
| | | * <p>统一执行链路大致分三段: |
| | | * 1. {@code resolveSharedRerouteDecision} 根据当前任务和场景先决策目标站。 |
| | | * 2. {@code buildRerouteCommandPlan} 决定用普通出库算路还是 run-block 专用算路。 |
| | | * 3. {@code executeReroutePlan} 按上下文里的开关决定是否做 suppress、是否加锁、 |
| | | * 是否先 cancel 旧 session、是否先清旧分段命令,然后真正下发。 |
| | | * |
| | | * <p>字段可以分成四组理解: |
| | | * 1. 场景与运行时对象: |
| | | * {@code sceneType} / {@code basDevp} / {@code stationThread} / |
| | | * {@code stationProtocol} / {@code wrkMast} |
| | | * 表示“谁在什么场景下触发了这次重算”。 |
| | | * 2. 目标决策输入: |
| | | * {@code outOrderStationIds} / {@code pathLenFactor} |
| | | * 表示“目标站如何算、路径倾向系数是多少”。 |
| | | * 3. 下发目标信息: |
| | | * {@code dispatchScene} / {@code dispatchDeviceNo} |
| | | * 表示“这次命令最终往哪个输送设备队列发,以及日志/去重场景名是什么”。 |
| | | * 4. 执行控制开关: |
| | | * {@code useRunBlockCommand} / {@code checkSuppressDispatch} / |
| | | * {@code requireOutOrderDispatchLock} / {@code cancelSessionBeforeDispatch} / |
| | | * {@code resetSegmentCommandsBeforeDispatch} / {@code clearIdleIssuedCommands} / |
| | | * {@code checkRecentDispatch} / {@code executionLockKey} / {@code executionLockSeconds} |
| | | * 表示“真正执行前后要打开哪些保护动作”。 |
| | | * |
| | | * <p>它本质上是一个参数对象加布尔开关集合: |
| | | * {@code create(...)} 先把这次 reroute 的基础现场填进来, |
| | | * 再通过 {@code withXxx(...)} 逐项声明这次执行需要附加哪些控制语义。 |
| | | * |
| | | * <p>例如: |
| | | * RUN_BLOCK_REROUTE 会打开 {@code useRunBlockCommand}, |
| | | * 并要求在下发前先 {@code cancelSession}、先清旧分段命令; |
| | | * OUT_ORDER 会打开 suppress guard 和 out-order lock; |
| | | * IDLE_RECOVER 则会打开 recent dispatch guard,并记录/清理停留期间已下发命令。 |
| | | */ |
| | | static final class RerouteContext { |
| | | // 本次 reroute 的触发来源。决定后面走哪类目标裁决、日志文案和特殊保护分支。 |
| | | private final RerouteSceneType sceneType; |
| | | // 当前输送设备配置,主要用于拿默认下发设备号和相关站点配置。 |
| | | private final BasDevp basDevp; |
| | | // 当前站点线程,后面构造输送命令时直接依赖它取 command。 |
| | | private final StationThread stationThread; |
| | | // 触发这次 reroute 的现场站点状态快照,包含 stationId/taskNo/runBlock/targetStaNo 等运行时信息。 |
| | | private final StationProtocol stationProtocol; |
| | | // 当前工作主表记录,表示这次重算对应的任务主状态。 |
| | | private final WrkMast wrkMast; |
| | | // 当前设备的出库排序点列表。目标站决策时需要知道哪些站点属于 out-order 节点。 |
| | | private final List<Integer> outOrderStationIds; |
| | | // 路径长度倾向系数。批次出库时用于让不同任务对路径选择有稳定偏好。 |
| | | private final Double pathLenFactor; |
| | | // 这次派发在日志、去重、session 记录中使用的场景名,例如 checkStationRunBlock_reroute。 |
| | | private final String dispatchScene; |
| | | // 实际投递命令的设备号。默认取 basDevp.getDevpNo(),某些场景可显式覆盖。 |
| | | private Integer dispatchDeviceNo; |
| | | // true 表示命令构造阶段改走 stationThread.getRunBlockRerouteCommand,而不是普通出库算路。 |
| | | private boolean useRunBlockCommand; |
| | | // true 表示执行前要先做 active-session suppress,避免旧活动路线被重复插入新命令。 |
| | | private boolean checkSuppressDispatch; |
| | | // true 表示执行前要加 out-order 专用短锁,防止同一排序点短时间重复计算/下发。 |
| | | private boolean requireOutOrderDispatchLock; |
| | | // true 表示真正下发前先取消旧 session。通常用于 reroute 替换旧路线。 |
| | | private boolean cancelSessionBeforeDispatch; |
| | | // true 表示真正下发前先清理旧分段输送命令,避免 segment executor 还持有旧路线。 |
| | | private boolean resetSegmentCommandsBeforeDispatch; |
| | | // true 表示要清理 idle recover 期间已经下发过但未实际生效的旧命令痕迹。 |
| | | private boolean clearIdleIssuedCommands; |
| | | // true 表示执行前要检查“最近刚下发过”,用于 idle recover 避免刚发完就重算。 |
| | | private boolean checkRecentDispatch; |
| | | // 可选执行锁 key。用于给某个 reroute 场景加短时间互斥。 |
| | | private String executionLockKey; |
| | | // executionLockKey 对应的锁秒数。 |
| | | private int executionLockSeconds; |
| | | // 仅 idle recover 需要,记录停留跟踪上下文,供清理旧命令与更新时间使用。 |
| | | private StationTaskIdleTrack idleTrack; |
| | | |
| | | private RerouteContext(RerouteSceneType sceneType, |
| | | BasDevp basDevp, |
| | | StationThread stationThread, |
| | | StationProtocol stationProtocol, |
| | | WrkMast wrkMast, |
| | | List<Integer> outOrderStationIds, |
| | | Double pathLenFactor, |
| | | String dispatchScene) { |
| | | this.sceneType = sceneType; |
| | | this.basDevp = basDevp; |
| | | this.stationThread = stationThread; |
| | | this.stationProtocol = stationProtocol; |
| | | this.wrkMast = wrkMast; |
| | | this.outOrderStationIds = outOrderStationIds == null ? Collections.emptyList() : outOrderStationIds; |
| | | this.pathLenFactor = pathLenFactor; |
| | | this.dispatchScene = dispatchScene; |
| | | this.dispatchDeviceNo = basDevp == null ? null : basDevp.getDevpNo(); |
| | | } |
| | | |
| | | static RerouteContext create(RerouteSceneType sceneType, |
| | | BasDevp basDevp, |
| | | StationThread stationThread, |
| | | StationProtocol stationProtocol, |
| | | WrkMast wrkMast, |
| | | List<Integer> outOrderStationIds, |
| | | Double pathLenFactor, |
| | | String dispatchScene) { |
| | | // create 只负责填基础现场,不默认打开任何执行开关。 |
| | | // 每个场景后面通过 withXxx 明确声明自己需要哪些附加控制。 |
| | | return new RerouteContext(sceneType, basDevp, stationThread, stationProtocol, wrkMast, outOrderStationIds, pathLenFactor, dispatchScene); |
| | | } |
| | | |
| | | RerouteContext withDispatchDeviceNo(Integer dispatchDeviceNo) { |
| | | // 覆盖默认下发设备号。典型场景是 out-order 站点配置的 deviceNo 与 basDevp 默认值不同。 |
| | | this.dispatchDeviceNo = dispatchDeviceNo; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withRunBlockCommand() { |
| | | // 命令构造阶段切换到 run-block 专用算路器。 |
| | | // 目标站仍由统一决策逻辑决定,只是“去目标站的路径”改为堵塞重规划算法生成。 |
| | | this.useRunBlockCommand = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withSuppressDispatchGuard() { |
| | | // 执行前启用 session suppress: |
| | | // 如果当前 task 在当前位置已经有一条活动中的同路径/同覆盖范围路线,则本次不再重复派发。 |
| | | this.checkSuppressDispatch = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withOutOrderDispatchLock() { |
| | | // 执行前启用排序点短锁。 |
| | | // 主要防止同一个 out-order/watch-circle 触发点在极短时间内被并发重复重算。 |
| | | this.requireOutOrderDispatchLock = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withCancelSessionBeforeDispatch() { |
| | | // 执行前显式取消旧 session。 |
| | | // 语义是“本次命令准备替换旧路线”,旧 routeVersion 之后不应再继续推进。 |
| | | this.cancelSessionBeforeDispatch = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withResetSegmentCommandsBeforeDispatch() { |
| | | // 执行前清掉 segment executor 侧旧分段命令。 |
| | | // 这对 run-block/idle recover 很关键,否则系统可能还拿着旧 segment 状态阻断新路线。 |
| | | this.resetSegmentCommandsBeforeDispatch = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext clearIdleIssuedCommands(StationTaskIdleTrack idleTrack) { |
| | | // 仅 idle recover 使用: |
| | | // 表示重启前要把“停留期间已经发过但可能未真正执行的命令痕迹”清理掉。 |
| | | this.clearIdleIssuedCommands = true; |
| | | this.idleTrack = idleTrack; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withRecentDispatchGuard() { |
| | | // 执行前检查最近是否刚下发过。 |
| | | // 避免 idle recover 在“刚重发完”的窗口内又马上触发一次。 |
| | | this.checkRecentDispatch = true; |
| | | return this; |
| | | } |
| | | |
| | | RerouteContext withExecutionLock(String executionLockKey, int executionLockSeconds) { |
| | | // 为某个场景挂一个独立执行锁。 |
| | | // 和 out-order lock 不同,这里是泛化锁,谁传 key 谁负责定义锁语义。 |
| | | this.executionLockKey = executionLockKey; |
| | | this.executionLockSeconds = executionLockSeconds; |
| | | return this; |
| | | } |
| | | |
| | | RerouteSceneType sceneType() { |
| | | return sceneType; |
| | | } |
| | | |
| | | BasDevp basDevp() { |
| | | return basDevp; |
| | | } |
| | | |
| | | StationThread stationThread() { |
| | | return stationThread; |
| | | } |
| | | |
| | | StationProtocol stationProtocol() { |
| | | return stationProtocol; |
| | | } |
| | | |
| | | WrkMast wrkMast() { |
| | | return wrkMast; |
| | | } |
| | | |
| | | List<Integer> outOrderStationIds() { |
| | | return outOrderStationIds; |
| | | } |
| | | |
| | | Double pathLenFactor() { |
| | | return pathLenFactor; |
| | | } |
| | | |
| | | String dispatchScene() { |
| | | return dispatchScene; |
| | | } |
| | | |
| | | Integer dispatchDeviceNo() { |
| | | return dispatchDeviceNo; |
| | | } |
| | | |
| | | boolean useRunBlockCommand() { |
| | | return useRunBlockCommand; |
| | | } |
| | | |
| | | boolean checkSuppressDispatch() { |
| | | return checkSuppressDispatch; |
| | | } |
| | | |
| | | boolean requireOutOrderDispatchLock() { |
| | | return requireOutOrderDispatchLock; |
| | | } |
| | | |
| | | boolean cancelSessionBeforeDispatch() { |
| | | return cancelSessionBeforeDispatch; |
| | | } |
| | | |
| | | boolean resetSegmentCommandsBeforeDispatch() { |
| | | return resetSegmentCommandsBeforeDispatch; |
| | | } |
| | | |
| | | boolean clearIdleIssuedCommands() { |
| | | return clearIdleIssuedCommands; |
| | | } |
| | | |
| | | boolean checkRecentDispatch() { |
| | | return checkRecentDispatch; |
| | | } |
| | | |
| | | String executionLockKey() { |
| | | return executionLockKey; |
| | | } |
| | | |
| | | int executionLockSeconds() { |
| | | return executionLockSeconds; |
| | | } |
| | | |
| | | StationTaskIdleTrack idleTrack() { |
| | | return idleTrack; |
| | | } |
| | | } |
| | | |
| | | static final class RerouteCommandPlan { |
| | | private final boolean skip; |
| | | private final String skipReason; |
| | | private final StationCommand command; |
| | | private final RerouteDecision decision; |
| | | private final String dispatchScene; |
| | | |
| | | private RerouteCommandPlan(boolean skip, |
| | | String skipReason, |
| | | StationCommand command, |
| | | RerouteDecision decision, |
| | | String dispatchScene) { |
| | | this.skip = skip; |
| | | this.skipReason = skipReason; |
| | | this.command = command; |
| | | this.decision = decision; |
| | | this.dispatchScene = dispatchScene; |
| | | } |
| | | |
| | | static RerouteCommandPlan skip(String reason) { |
| | | return new RerouteCommandPlan(true, reason, null, null, null); |
| | | } |
| | | |
| | | static RerouteCommandPlan dispatch(StationCommand command, |
| | | RerouteDecision decision, |
| | | String dispatchScene) { |
| | | return new RerouteCommandPlan(false, null, command, decision, dispatchScene); |
| | | } |
| | | |
| | | boolean skip() { |
| | | return skip; |
| | | } |
| | | |
| | | String skipReason() { |
| | | return skipReason; |
| | | } |
| | | |
| | | StationCommand command() { |
| | | return command; |
| | | } |
| | | |
| | | RerouteDecision decision() { |
| | | return decision; |
| | | } |
| | | |
| | | String dispatchScene() { |
| | | return dispatchScene; |
| | | } |
| | | } |
| | | |
| | | static final class RerouteExecutionResult { |
| | | private final boolean skipped; |
| | | private final String skipReason; |
| | | private final boolean dispatched; |
| | | private final StationCommand command; |
| | | private final int clearedCommandCount; |
| | | |
| | | private RerouteExecutionResult(boolean skipped, |
| | | String skipReason, |
| | | boolean dispatched, |
| | | StationCommand command, |
| | | int clearedCommandCount) { |
| | | this.skipped = skipped; |
| | | this.skipReason = skipReason; |
| | | this.dispatched = dispatched; |
| | | this.command = command; |
| | | this.clearedCommandCount = clearedCommandCount; |
| | | } |
| | | |
| | | static RerouteExecutionResult skip(String reason) { |
| | | return new RerouteExecutionResult(true, reason, false, null, 0); |
| | | } |
| | | |
| | | static RerouteExecutionResult dispatched(StationCommand command, |
| | | int clearedCommandCount) { |
| | | return new RerouteExecutionResult(false, null, true, command, clearedCommandCount); |
| | | } |
| | | |
| | | boolean skipped() { |
| | | return skipped; |
| | | } |
| | | |
| | | String skipReason() { |
| | | return skipReason; |
| | | } |
| | | |
| | | boolean dispatched() { |
| | | return dispatched; |
| | | } |
| | | |
| | | StationCommand command() { |
| | | return command; |
| | | } |
| | | |
| | | int clearedCommandCount() { |
| | | return clearedCommandCount; |
| | | } |
| | | } |
| | | |
| | | private static class OutOrderDispatchDecision { |
| | | private final Integer targetStationId; |
| | | private final boolean circle; |
| | | private final StationTaskLoopService.LoopEvaluation loopEvaluation; |
| | | private final boolean countLoopIssue; |
| | | |
| | | private OutOrderDispatchDecision(Integer targetStationId, boolean circle) { |
| | | this(targetStationId, circle, null, false); |
| | | } |
| | | |
| | | private OutOrderDispatchDecision(Integer targetStationId, |
| | | boolean circle, |
| | | StationTaskLoopService.LoopEvaluation loopEvaluation, |
| | | boolean countLoopIssue) { |
| | | this.targetStationId = targetStationId; |
| | | this.circle = circle; |
| | | this.loopEvaluation = loopEvaluation; |
| | | this.countLoopIssue = countLoopIssue; |
| | | } |
| | | |
| | | private Integer getTargetStationId() { |
| | |
| | | private boolean isCircle() { |
| | | return circle; |
| | | } |
| | | |
| | | private StationTaskLoopService.LoopEvaluation getLoopEvaluation() { |
| | | return loopEvaluation; |
| | | } |
| | | |
| | | private boolean shouldCountLoopIssue() { |
| | | return countLoopIssue; |
| | | } |
| | | } |
| | | |
| | | private static class CircleTargetCandidate { |
| | | private final Integer stationId; |
| | | private final Integer pathLength; |
| | | private final Integer offset; |
| | | |
| | | private CircleTargetCandidate(Integer stationId, Integer pathLength, Integer offset) { |
| | | this.stationId = stationId; |
| | | this.pathLength = pathLength == null ? 0 : pathLength; |
| | | this.offset = offset == null ? 0 : offset; |
| | | } |
| | | |
| | | private Integer getStationId() { |
| | | return stationId; |
| | | } |
| | | |
| | | private Integer getPathLength() { |
| | | return pathLength; |
| | | } |
| | | |
| | | private Integer getOffset() { |
| | | return offset; |
| | | } |
| | | } |
| | | |
| | | private void saveLoopLoadReserve(Integer wrkNo, LoopHitResult loopHitResult) { |