| | |
| | | private S7SerializerProvider s7SerializerProvider; |
| | | |
| | | // MES字段列表(进片和出片共用同一套协议) |
| | | // 根据协议,使用带数字后缀的字段名(1-6对应6个玻璃位置) |
| | | // 支持读取所有6个玻璃的信息 |
| | | private static final List<String> MES_FIELDS = Arrays.asList( |
| | | "mesSend", "mesGlassId", "mesWidth", "mesHeight", |
| | | "startSlot", "targetSlot", "workLine" |
| | | "mesSend", "mesConfirm", |
| | | // 玻璃1-6的ID |
| | | "mesGlassId1", "mesGlassId2", "mesGlassId3", "mesGlassId4", "mesGlassId5", "mesGlassId6", |
| | | // 玻璃1-6的尺寸 |
| | | "mesWidth1", "mesWidth2", "mesWidth3", "mesWidth4", "mesWidth5", "mesWidth6", |
| | | "mesHeight1", "mesHeight2", "mesHeight3", "mesHeight4", "mesHeight5", "mesHeight6", |
| | | "mesThickness1", "mesThickness2", "mesThickness3", "mesThickness4", "mesThickness5", "mesThickness6", |
| | | // 玻璃1-6的起始位置和目标位置 |
| | | "start1", "start2", "start3", "start4", "start5", "start6", |
| | | "target1", "target2", "target3", "target4", "target5", "target6" |
| | | ); |
| | | |
| | | // 监控线程池:用于定期检查大车状态并协调卧转立设备 |
| | |
| | | if (needsStateCheck(operation)) { |
| | | VehicleStatus status = statusManager.getVehicleStatus(deviceId); |
| | | if (status != null && !status.isAvailable()) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message(String.format("车辆 %s (%s) 当前状态为 %s,无法执行操作 %s", |
| | | deviceConfig.getDeviceName(), |
| | | deviceId, |
| | | status.getState(), |
| | | operation)) |
| | | .build(); |
| | | // 对于 feedGlass 操作,如果状态是 EXECUTING,允许继续执行 |
| | | // 因为定时器可能会重复调用,或者需要等待任务完成 |
| | | if ("feedGlass".equals(operation) && status.isExecuting()) { |
| | | log.debug("车辆 {} 当前状态为 EXECUTING,但允许继续执行 feedGlass(定时器重复调用)", deviceId); |
| | | // 允许继续执行,不返回错误 |
| | | } else { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message(String.format("车辆 %s (%s) 当前状态为 %s,无法执行操作 %s", |
| | | deviceConfig.getDeviceName(), |
| | | deviceId, |
| | | status.getState(), |
| | | operation)) |
| | | .build(); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | break; |
| | | case "setOnlineState": |
| | | result = handleSetOnlineState(deviceConfig, params, logicParams); |
| | | break; |
| | | case "checkMesConfirm": |
| | | result = checkMesConfirm(deviceConfig, logicParams); |
| | | break; |
| | | case "markBroken": |
| | | result = handleMarkBroken(deviceConfig, params, logicParams); |
| | | break; |
| | | default: |
| | | log.warn("不支持的操作类型: {}", operation); |
| | |
| | | // 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取) |
| | | Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000); |
| | | Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm |
| | | Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true); |
| | | Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5); |
| | | Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true); // 自动上料:是否自动触发上料请求(写入plcRequest=1) |
| | | |
| | | // 从运行时参数中获取数据(从接口调用时传入) |
| | | List<GlassInfo> glassInfos = extractGlassInfos(params); |
| | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 0); |
| | | payload.put("plcReport", 0); |
| | | // 清空state1~6,避免遗留状态导致误判 |
| | | for (int i = 1; i <= 6; i++) { |
| | | payload.put("state" + i, 0); |
| | | } |
| | | payload.put("onlineState", Boolean.TRUE); |
| | | |
| | | log.info("大车设备重置: deviceId={}", deviceConfig.getId()); |
| | |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | handleStopTaskMonitor(deviceConfig); |
| | | handleStopIdleMonitor(deviceConfig); |
| | | updateDeviceOnlineStatus(deviceConfig, true); |
| | | } else { |
| | | // 即便重置失败,也尝试停止内部监控,避免任务取消后仍然反复写PLC |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | handleStopTaskMonitor(deviceConfig); |
| | | handleStopIdleMonitor(deviceConfig); |
| | | } |
| | | |
| | | return result; |
| | |
| | | payload.put(field, ""); |
| | | } |
| | | |
| | | payload.put("plcGlassCount", 0); |
| | | payload.put("plcRequest", 0); |
| | | payload.put("plcReport", 0); |
| | | payload.put("onlineState", Boolean.TRUE); |
| | |
| | | payload, |
| | | "大车设备-清空玻璃数据" |
| | | ); |
| | | |
| | | // 同步清空state1~6等动态字段(在动态数据区中) |
| | | clearDynamicTaskStates(deviceConfig); |
| | | |
| | | // 清空后,恢复为空闲状态,停止监控 |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | handleStopTaskMonitor(deviceConfig); |
| | | handleStopIdleMonitor(deviceConfig); |
| | | updateDeviceOnlineStatus(deviceConfig, true); |
| | | } else { |
| | | // 写入失败也尝试停止监控,避免任务取消后仍旧运行 |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | handleStopTaskMonitor(deviceConfig); |
| | | handleStopIdleMonitor(deviceConfig); |
| | | } |
| | | |
| | | return result; |
| | |
| | | Map<String, Object> defaultParams = new HashMap<>(); |
| | | defaultParams.put("vehicleCapacity", 6000); |
| | | defaultParams.put("glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm |
| | | defaultParams.put("autoFeed", true); |
| | | defaultParams.put("autoFeed", true); // 自动上料:是否自动触发上料请求(写入plcRequest=1),默认true |
| | | defaultParams.put("maxRetryCount", 5); |
| | | defaultParams.put("defaultGlassLength", 2000); |
| | | |
| | | // MES任务相关配置 |
| | | defaultParams.put("vehicleSpeed", 1.0); // 车辆速度(格/秒,grid/s),默认1格/秒 |
| | |
| | | * @param vehicleCapacity 车辆容量(mm) |
| | | * @param glassGap 玻璃之间的物理间隔(mm),默认200mm |
| | | * @param deviceId 设备ID(用于日志) |
| | | * @return 规划后的玻璃列表,如果玻璃没有长度则返回null(用于测试MES程序) |
| | | * @return 规划后的玻璃列表,如果玻璃没有长度则返回null(MES未提供长度数据) |
| | | */ |
| | | private List<GlassInfo> planGlassLoading(List<GlassInfo> source, |
| | | int vehicleCapacity, |
| | |
| | | for (GlassInfo info : source) { |
| | | Integer glassLength = info.getLength(); |
| | | |
| | | // 如果玻璃没有长度,说明MES未提供,直接报错 |
| | | if (glassLength == null || glassLength <= 0) { |
| | | // 玻璃没有长度,直接报错(用于测试MES程序) |
| | | log.error("玻璃[{}]缺少长度数据,无法进行容量计算。deviceId={},请检查MES程序是否正确提供玻璃长度。", |
| | | info.getGlassId(), deviceId); |
| | | return null; |
| | |
| | | } |
| | | |
| | | try { |
| | | // 检查是否已有任务在执行中 |
| | | MesTaskInfo existingTask = currentTasks.get(deviceId); |
| | | if (existingTask != null) { |
| | | log.debug("设备已有任务在执行中,跳过检查MES任务: deviceId={}", deviceId); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("任务执行中,无需重复检查MES任务") |
| | | .data(Collections.singletonMap("waiting", false)) |
| | | .build(); |
| | | } |
| | | |
| | | // 读取MES字段(进片和出片共用) |
| | | Map<String, Object> mesData = plcDynamicDataService.readPlcData( |
| | | deviceConfig, MES_FIELDS, serializer); |
| | | if (mesData == null || mesData.isEmpty()) { |
| | | log.warn("读取MES字段失败: deviceId={}, mesData为空或null", deviceId); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("读取MES字段失败") |
| | | .build(); |
| | | } |
| | | |
| | | |
| | | // 解析mesSend |
| | | Integer mesSend = parseInteger(mesData.get("mesSend")); |
| | | |
| | | if (mesSend == null || mesSend == 0) { |
| | | Map<String, Object> waitData = new HashMap<>(); |
| | | waitData.put("completed", false); |
| | | waitData.put("waiting", true); |
| | | waitData.put("waitingReason", "mesSend=0"); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("暂无MES任务(mesSend=0)") |
| | | .message("等待MES发送请求(mesSend=0)") |
| | | .data(waitData) |
| | | .build(); |
| | | } |
| | | |
| | | // mesSend=1,读取任务参数 |
| | | String glassId = parseString(mesData.get("mesGlassId")); |
| | | Integer startSlot = parseInteger(mesData.get("startSlot")); // 起始位置编号 |
| | | Integer targetSlot = parseInteger(mesData.get("targetSlot")); // 目标位置编号 |
| | | Integer workLine = parseInteger(mesData.get("workLine")); |
| | | // mesSend=1,记录日志 |
| | | log.info("检测到mesSend=1,开始读取MES任务信息: deviceId={}", deviceId); |
| | | |
| | | if (glassId == null || glassId.isEmpty()) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("MES未提供玻璃ID") |
| | | .build(); |
| | | // mesSend=1,读取所有玻璃的任务参数(支持1-6个玻璃) |
| | | List<GlassTaskInfo> glasses = new ArrayList<>(); |
| | | boolean isOutbound = false; // 将在处理第一个有效玻璃时确定 |
| | | |
| | | // 读取所有6个玻璃的信息 |
| | | for (int i = 1; i <= 6; i++) { |
| | | String glassId = parseString(mesData.get("mesGlassId" + i)); |
| | | if (glassId == null || glassId.isEmpty()) { |
| | | continue; // 跳过空的玻璃ID |
| | | } |
| | | |
| | | Integer startSlot = parseInteger(mesData.get("start" + i)); |
| | | Integer targetSlot = parseInteger(mesData.get("target" + i)); |
| | | Integer width = parseInteger(mesData.get("mesWidth" + i)); |
| | | Integer height = parseInteger(mesData.get("mesHeight" + i)); |
| | | Integer thickness = parseInteger(mesData.get("mesThickness" + i)); |
| | | |
| | | log.debug("读取玻璃{}信息: deviceId={}, glassId={}, startSlot={}, targetSlot={}, width={}, height={}, thickness={}", |
| | | i, deviceId, glassId, startSlot, targetSlot, width, height, thickness); |
| | | |
| | | if (startSlot == null || targetSlot == null) { |
| | | log.warn("玻璃{}信息不完整,跳过: glassId={}, startSlot={}, targetSlot={}", |
| | | i, glassId, startSlot, targetSlot); |
| | | continue; |
| | | } |
| | | |
| | | // 对于第一个有效玻璃,判断是进片还是出片任务 |
| | | if (glasses.isEmpty()) { |
| | | isOutbound = isOutboundTask(startSlot, logicParams); |
| | | } |
| | | |
| | | // 位置映射 |
| | | Integer startPosition; |
| | | if (isOutbound) { |
| | | // 出片任务:startSlot是格子编号,需要映射到实际位置 |
| | | startPosition = mapOutboundPosition(startSlot, logicParams); |
| | | } else { |
| | | // 进片任务:startSlot是卧转立编号,通过positionMapping映射 |
| | | startPosition = mapPosition(startSlot, logicParams); |
| | | } |
| | | |
| | | // targetSlot统一通过positionMapping映射 |
| | | Integer targetPosition = mapPosition(targetSlot, logicParams); |
| | | |
| | | if (startPosition == null || targetPosition == null) { |
| | | log.warn("玻璃{}位置映射失败,跳过: glassId={}, startSlot={}, targetSlot={}", |
| | | i, glassId, startSlot, targetSlot); |
| | | continue; |
| | | } |
| | | |
| | | // 创建玻璃任务信息 |
| | | GlassTaskInfo glassInfo = new GlassTaskInfo(); |
| | | glassInfo.glassId = glassId; |
| | | glassInfo.startSlot = startSlot; |
| | | glassInfo.targetSlot = targetSlot; |
| | | glassInfo.startPosition = startPosition; |
| | | glassInfo.targetPosition = targetPosition; |
| | | glassInfo.width = width; |
| | | glassInfo.height = height; |
| | | glassInfo.thickness = thickness; |
| | | glasses.add(glassInfo); |
| | | } |
| | | |
| | | // 判断是进片还是出片任务 |
| | | // 方法:通过startSlot判断 |
| | | // - 如果startSlot是卧转立编号(如900/901),则是进片任务 |
| | | // - 如果startSlot是格子编号(在大理片笼范围内),则是出片任务 |
| | | boolean isOutbound = isOutboundTask(startSlot, logicParams); |
| | | |
| | | // 位置映射 |
| | | Integer startPosition; |
| | | if (isOutbound) { |
| | | // 出片任务:startSlot是格子编号,需要映射到实际位置 |
| | | startPosition = mapOutboundPosition(startSlot, logicParams); |
| | | } else { |
| | | // 进片任务:startSlot是卧转立编号,通过positionMapping映射 |
| | | startPosition = mapPosition(startSlot, logicParams); |
| | | } |
| | | |
| | | // targetSlot统一通过positionMapping映射 |
| | | Integer targetPosition = mapPosition(targetSlot, logicParams); |
| | | |
| | | if (startPosition == null || targetPosition == null) { |
| | | if (glasses.isEmpty()) { |
| | | // 记录详细的MES数据,帮助调试 |
| | | log.warn("MES未提供有效的玻璃信息: deviceId={}, mesSend={}, mesData={}", |
| | | deviceId, mesSend, mesData); |
| | | Map<String, Object> waitData = new HashMap<>(); |
| | | waitData.put("completed", false); |
| | | waitData.put("waiting", true); |
| | | waitData.put("waitingReason", "glassInfoMissing"); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message(String.format("位置映射失败: startSlot=%s, targetSlot=%s, isOutbound=%s", |
| | | startSlot, targetSlot, isOutbound)) |
| | | .message("MES未提供有效的玻璃信息(mesSend=1但未找到有效的glassId、startSlot、targetSlot)") |
| | | .data(waitData) |
| | | .build(); |
| | | } |
| | | |
| | | // 读取当前位置 |
| | | Integer currentPosition = getCurrentPosition(deviceConfig, logicParams); |
| | | |
| | | // 计算时间 |
| | | // 使用第一个玻璃的位置计算时间(所有玻璃使用相同的路径) |
| | | GlassTaskInfo firstGlass = glasses.get(0); |
| | | TimeCalculation timeCalc = calculateTime( |
| | | currentPosition, startPosition, targetPosition, logicParams); |
| | | currentPosition, firstGlass.startPosition, firstGlass.targetPosition, logicParams); |
| | | |
| | | // 创建任务信息 |
| | | MesTaskInfo taskInfo = new MesTaskInfo(); |
| | | taskInfo.glassId = glassId; |
| | | taskInfo.startSlot = startSlot; |
| | | taskInfo.targetSlot = targetSlot; |
| | | taskInfo.startPosition = startPosition; |
| | | taskInfo.targetPosition = targetPosition; |
| | | taskInfo.glasses = glasses; |
| | | taskInfo.currentPosition = currentPosition; |
| | | taskInfo.gotime = timeCalc.gotime; |
| | | taskInfo.cartime = timeCalc.cartime; |
| | | taskInfo.workLine = workLine; |
| | | taskInfo.createdTime = System.currentTimeMillis(); |
| | | taskInfo.isOutbound = isOutbound; |
| | | |
| | | // 从配置中读取破损玻璃索引(用于测试场景) |
| | | // 配置格式:brokenGlassIndices: [0, 2] 表示第1个和第3个玻璃应该破损 |
| | | @SuppressWarnings("unchecked") |
| | | List<Integer> brokenIndices = getLogicParam(logicParams, "brokenGlassIndices", null); |
| | | if (brokenIndices != null && !brokenIndices.isEmpty()) { |
| | | taskInfo.brokenGlassIndices = new ArrayList<>(); |
| | | for (Integer index : brokenIndices) { |
| | | if (index != null && index >= 0 && index < glasses.size()) { |
| | | taskInfo.brokenGlassIndices.add(index); |
| | | } |
| | | } |
| | | if (!taskInfo.brokenGlassIndices.isEmpty()) { |
| | | log.info("任务创建时标记破损玻璃: deviceId={}, brokenIndices={}", |
| | | deviceId, taskInfo.brokenGlassIndices); |
| | | } |
| | | } |
| | | |
| | | currentTasks.put(deviceId, taskInfo); |
| | | |
| | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 0); |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | log.info("已清空plcRequest=0: deviceId={}", deviceId); |
| | | |
| | | // 更新车辆状态为执行中 |
| | | statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING); |
| | | |
| | | // 启动任务监控 |
| | | handleStartTaskMonitor(deviceConfig, params, logicParams); |
| | | log.info("已启动任务监控: deviceId={}", deviceId); |
| | | |
| | | String taskType = isOutbound ? "出片" : "进片"; |
| | | log.info("MES{}任务已创建: deviceId={}, glassId={}, startSlot={}(位置{}格), targetSlot={}(位置{}格), 距离{}格->{}格, gotime={}ms({}秒), cartime={}ms({}秒)", |
| | | taskType, deviceId, glassId, startSlot, startPosition, targetSlot, targetPosition, |
| | | Math.abs(startPosition - currentPosition), Math.abs(targetPosition - startPosition), |
| | | String glassIds = glasses.stream() |
| | | .map(g -> g.glassId) |
| | | .collect(java.util.stream.Collectors.joining(",")); |
| | | log.info("MES{}任务已创建: deviceId={}, glassCount={}, glassIds=[{}], 起始位置={}格, 目标位置={}格, 距离{}格->{}格, gotime={}ms({}秒), cartime={}ms({}秒)", |
| | | taskType, deviceId, glasses.size(), glassIds, |
| | | firstGlass.startPosition, firstGlass.targetPosition, |
| | | Math.abs(firstGlass.startPosition - currentPosition), |
| | | Math.abs(firstGlass.targetPosition - firstGlass.startPosition), |
| | | timeCalc.gotime, timeCalc.gotime / 1000.0, timeCalc.cartime, timeCalc.cartime / 1000.0); |
| | | |
| | | // 构建详细的步骤提示信息 |
| | | StringBuilder stepMessage = new StringBuilder(); |
| | | stepMessage.append("检测到MES任务(mesSend=1),已清空请求字(plcRequest=0)"); |
| | | stepMessage.append(";开始计算时间,预计到达起始位置耗时").append(timeCalc.gotime / 1000.0).append("秒"); |
| | | stepMessage.append(",运输到目标位置耗时").append(timeCalc.cartime / 1000.0).append("秒"); |
| | | if (!isOutbound) { |
| | | stepMessage.append(";进片任务:等待玻璃上车后,将告知卧转立设备"); |
| | | } |
| | | |
| | | Map<String, Object> successData = new HashMap<>(); |
| | | successData.put("waiting", false); |
| | | successData.put("taskStarted", true); |
| | | |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message(String.format("MES%s任务已创建: glassId=%s, start=%d, target=%d", |
| | | taskType, glassId, startPosition, targetPosition)) |
| | | .message(stepMessage.toString()) |
| | | .data(successData) |
| | | .build(); |
| | | |
| | | } catch (Exception e) { |
| | |
| | | } |
| | | |
| | | // 获取速度(格/秒,grid/s) |
| | | Double speed = getLogicParam(logicParams, "vehicleSpeed", 1.0); |
| | | // 这里不能直接用 Double 泛型,否则当配置里是 Integer 时会出现 |
| | | // java.lang.Integer cannot be cast to java.lang.Double 的异常 |
| | | Object speedObj = getLogicParam(logicParams, "vehicleSpeed", 1.0); |
| | | Double speed = null; |
| | | if (speedObj instanceof Double) { |
| | | speed = (Double) speedObj; |
| | | } else if (speedObj instanceof Integer) { |
| | | speed = ((Integer) speedObj).doubleValue(); |
| | | } else if (speedObj instanceof Number) { |
| | | speed = ((Number) speedObj).doubleValue(); |
| | | } else { |
| | | try { |
| | | speed = Double.parseDouble(String.valueOf(speedObj)); |
| | | } catch (Exception ignore) { |
| | | } |
| | | } |
| | | if (speed == null || speed <= 0) { |
| | | speed = 1.0; // 默认1格/秒 |
| | | } |
| | |
| | | long state1Time = taskInfo.gotime; // 到达起始位置,上车完成 |
| | | long state2Time = taskInfo.gotime + taskInfo.cartime; // 到达目标位置,运输完成 |
| | | |
| | | // 更新state状态 |
| | | if (elapsed >= state1Time && elapsed < state2Time) { |
| | | // state应该为1 |
| | | if (taskInfo.isOutbound) { |
| | | // 出片任务:到达源位置(大理片笼),取片完成 |
| | | updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo); |
| | | } else { |
| | | // 进片任务:到达起始位置(卧转立),上车完成 |
| | | updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo); |
| | | } |
| | | } else if (elapsed >= state2Time) { |
| | | // state应该为2(运输完成) |
| | | updateStateIfNeeded(deviceConfig, serializer, stateValues, 2, taskInfo); |
| | | // 获取超时时间配置(默认:超过预期完成时间的200%视为超时未完成) |
| | | Double timeoutRatio = getLogicParam(logicParams, "state3TimeoutRatio", 2.0); |
| | | long state3TimeoutTime = (long) (state2Time * timeoutRatio); // 超时时间点 |
| | | |
| | | // 更新state状态(每个玻璃对应一个state) |
| | | int glassCount = taskInfo.glasses.size(); |
| | | boolean hasStateOne = false; // 标记是否有state变为1 |
| | | boolean hasStateTwo = false; // 标记是否有state变为2 |
| | | String currentStepDesc = ""; // 当前步骤描述 |
| | | |
| | | for (int i = 0; i < glassCount && i < 6; i++) { |
| | | String stateField = "state" + (i + 1); |
| | | Object currentValue = stateValues.get(stateField); |
| | | Integer currentState = parseInteger(currentValue); |
| | | |
| | | // 检查是否所有state都>=2,如果是则给MES汇报 |
| | | if (allStatesCompleted(stateValues)) { |
| | | reportToMes(deviceConfig, serializer, taskInfo, logicParams); |
| | | // 如果当前state已经是3(未完成)或8(破损),跳过 |
| | | if (currentState != null && (currentState == 3 || currentState == 8)) { |
| | | continue; |
| | | } |
| | | |
| | | // 优先检查是否标记为破损(state=8) |
| | | // 检查任务信息中是否标记了该玻璃为破损 |
| | | if (taskInfo.brokenGlassIndices != null && taskInfo.brokenGlassIndices.contains(i)) { |
| | | updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 8, taskInfo); |
| | | log.info("玻璃标记为破损: deviceId={}, stateField={}, glassIndex={}", |
| | | deviceConfig.getDeviceId(), stateField, i); |
| | | continue; |
| | | } |
| | | |
| | | // 检查超时未完成(state=3) |
| | | if (elapsed >= state3TimeoutTime && (currentState == null || currentState < 2)) { |
| | | updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 3, taskInfo); |
| | | log.warn("任务超时未完成: deviceId={}, stateField={}, elapsed={}ms, expectedTime={}ms", |
| | | deviceConfig.getDeviceId(), stateField, elapsed, state2Time); |
| | | continue; |
| | | } |
| | | |
| | | // 正常状态更新 |
| | | if (elapsed >= state1Time && elapsed < state2Time) { |
| | | // state应该为1(上车完成) |
| | | boolean stateChanged = updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 1, taskInfo); |
| | | if (stateChanged) { |
| | | hasStateOne = true; |
| | | currentStepDesc = "玻璃已上车(state=1),正在运输到目标位置"; |
| | | } else if (currentState != null && currentState == 1) { |
| | | currentStepDesc = "玻璃已上车(state=1),正在运输到目标位置"; |
| | | } |
| | | } else if (elapsed >= state2Time) { |
| | | // state应该为2(运输完成) |
| | | boolean stateChanged = updateStateIfNeeded(deviceConfig, serializer, stateValues, stateField, 2, taskInfo); |
| | | if (stateChanged) { |
| | | hasStateTwo = true; |
| | | currentStepDesc = "玻璃已到达目标位置(state=2),等待MES确认"; |
| | | } else if (currentState != null && currentState == 2) { |
| | | currentStepDesc = "玻璃已到达目标位置(state=2),等待MES确认"; |
| | | } |
| | | } else { |
| | | // 还在前往起始位置 |
| | | currentStepDesc = "正在前往起始位置,预计耗时" + (state1Time / 1000.0) + "秒"; |
| | | } |
| | | } |
| | | |
| | | // 当state变为1时(玻璃上车完成),清空卧转立设备的plcRequest(只执行一次) |
| | | if (hasStateOne && !taskInfo.transferPlcRequestCleared) { |
| | | clearTransferPlcRequest(deviceConfig, logicParams); |
| | | taskInfo.transferPlcRequestCleared = true; // 标记已清空,避免重复执行 |
| | | if (!taskInfo.isOutbound) { |
| | | currentStepDesc = "玻璃已上车(state=1),已告知卧转立设备(plcRequest=0),正在运输到目标位置"; |
| | | } |
| | | } |
| | | |
| | | // 检查是否所有state都>=2,如果是则给MES汇报 |
| | | if (elapsed >= state2Time && allStatesCompleted(stateValues, glassCount)) { |
| | | reportToMes(deviceConfig, serializer, taskInfo, logicParams); |
| | | // 记录MES确认开始等待的时间(只记录一次) |
| | | if (taskInfo.mesConfirmStartTime == null) { |
| | | taskInfo.mesConfirmStartTime = System.currentTimeMillis(); |
| | | currentStepDesc = "玻璃已到达目标位置(state=2),已发送汇报(plcReport=1),等待MES确认"; |
| | | } |
| | | } |
| | | |
| | | // 保存当前步骤描述到任务信息中,供checkMesConfirm使用 |
| | | if (!currentStepDesc.isEmpty()) { |
| | | taskInfo.currentStepDesc = currentStepDesc; |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | |
| | | |
| | | /** |
| | | * 更新state状态(如果需要) |
| | | * @return 是否发生了状态变化(从非目标状态变为目标状态) |
| | | */ |
| | | private void updateStateIfNeeded(DeviceConfig deviceConfig, |
| | | private boolean updateStateIfNeeded(DeviceConfig deviceConfig, |
| | | EnhancedS7Serializer serializer, |
| | | Map<String, Object> currentStates, |
| | | String stateField, |
| | | int targetState, |
| | | MesTaskInfo taskInfo) { |
| | | |
| | | // 这里可以根据实际需求更新state字段 |
| | | // 暂时只记录日志,实际更新可能需要根据具体PLC字段配置 |
| | | log.debug("任务状态更新: deviceId={}, targetState={}", |
| | | deviceConfig.getDeviceId(), targetState); |
| | | // 检查当前state值 |
| | | Object currentValue = currentStates.get(stateField); |
| | | Integer currentState = parseInteger(currentValue); |
| | | |
| | | // 如果当前state小于目标state,则更新 |
| | | // 注意:如果当前state已经是3(未完成)或8(破损),不再更新 |
| | | if (currentState != null && (currentState == 3 || currentState == 8)) { |
| | | log.debug("任务状态已为异常状态,不再更新: deviceId={}, stateField={}, currentState={}, targetState={}", |
| | | deviceConfig.getDeviceId(), stateField, currentState, targetState); |
| | | return false; |
| | | } |
| | | |
| | | if (currentState == null || currentState < targetState) { |
| | | // 实际写入PLC的state字段 |
| | | try { |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put(stateField, targetState); |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | |
| | | log.info("任务状态已更新到PLC: deviceId={}, stateField={}, currentState={}, targetState={}", |
| | | deviceConfig.getDeviceId(), stateField, currentState, targetState); |
| | | // 返回true表示状态发生了变化 |
| | | return true; |
| | | } catch (Exception e) { |
| | | log.error("写入PLC state字段失败: deviceId={}, stateField={}, targetState={}, error={}", |
| | | deviceConfig.getDeviceId(), stateField, targetState, e.getMessage()); |
| | | return false; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 清空卧转立设备的plcRequest(当大车state=1时,表示玻璃已上车) |
| | | */ |
| | | private void clearTransferPlcRequest(DeviceConfig deviceConfig, Map<String, Object> logicParams) { |
| | | try { |
| | | // 查找同组的卧转立设备 |
| | | List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig); |
| | | if (transferDevices.isEmpty()) { |
| | | log.debug("未找到同组的卧转立设备,跳过清空plcRequest: deviceId={}", deviceConfig.getId()); |
| | | return; |
| | | } |
| | | |
| | | // 将每个卧转立设备的 plcRequest 置 0 |
| | | for (DeviceConfig transferDevice : transferDevices) { |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 0); |
| | | |
| | | DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields( |
| | | transferDevice.getId(), |
| | | payload, |
| | | "大车state=1自动清空卧转立请求" |
| | | ); |
| | | |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | log.info("已自动清空卧转立设备 plcRequest: vehicleDeviceId={}, transferDeviceId={}, transferDeviceName={}", |
| | | deviceConfig.getId(), transferDevice.getId(), transferDevice.getDeviceName()); |
| | | } else { |
| | | log.warn("自动清空卧转立设备 plcRequest 失败: vehicleDeviceId={}, transferDeviceId={}, message={}", |
| | | deviceConfig.getId(), transferDevice.getId(), result.getMessage()); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("清空卧转立设备plcRequest异常: deviceId={}", deviceConfig.getId(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 检查是否所有state都已完成(>=2) |
| | | */ |
| | | private boolean allStatesCompleted(Map<String, Object> stateValues) { |
| | | for (Object value : stateValues.values()) { |
| | | private boolean allStatesCompleted(Map<String, Object> stateValues, int glassCount) { |
| | | // 只检查实际有玻璃的state字段 |
| | | for (int i = 1; i <= glassCount && i <= 6; i++) { |
| | | String stateField = "state" + i; |
| | | Object value = stateValues.get(stateField); |
| | | Integer state = parseInteger(value); |
| | | if (state == null || state < 2) { |
| | | return false; |
| | | } |
| | | } |
| | | return !stateValues.isEmpty(); |
| | | return glassCount > 0; |
| | | } |
| | | |
| | | /** |
| | |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | |
| | | String taskType = taskInfo.isOutbound ? "出片" : "进片"; |
| | | log.info("已给MES汇报({}任务): deviceId={}, glassId={}", |
| | | taskType, deviceConfig.getDeviceId(), taskInfo.glassId); |
| | | String glassIds = taskInfo.glasses.stream() |
| | | .map(g -> g.glassId) |
| | | .collect(java.util.stream.Collectors.joining(",")); |
| | | log.info("已给MES汇报({}任务): deviceId={}, glassCount={}, glassIds=[{}]", |
| | | taskType, deviceConfig.getDeviceId(), taskInfo.glasses.size(), glassIds); |
| | | |
| | | // 多设备任务场景下,不在这里阻塞等待MES确认,由任务引擎定时调用checkMesConfirm |
| | | } catch (Exception e) { |
| | |
| | | .build(); |
| | | } |
| | | |
| | | String deviceId = deviceConfig.getDeviceId(); |
| | | MesTaskInfo taskInfo = currentTasks.get(deviceId); |
| | | |
| | | // 如果没有任务记录,优先尝试补偿性地检查一次MES任务(避免因时序问题一直noTask) |
| | | if (taskInfo == null) { |
| | | log.info("检查MES确认时未找到任务记录,尝试补偿检查MES任务: deviceId={}", deviceId); |
| | | try { |
| | | DevicePlcVO.OperationResult checkResult = |
| | | handleCheckMesTask(deviceConfig, Collections.emptyMap(), logicParams); |
| | | if (Boolean.TRUE.equals(checkResult.getSuccess())) { |
| | | taskInfo = currentTasks.get(deviceId); |
| | | if (taskInfo != null) { |
| | | log.info("补偿检查MES任务成功,已创建任务记录: deviceId={}", deviceId); |
| | | } else { |
| | | log.info("补偿检查MES任务执行成功但未创建任务记录: deviceId={}, message={}", |
| | | deviceId, checkResult.getMessage()); |
| | | } |
| | | } else { |
| | | log.warn("补偿检查MES任务失败: deviceId={}, message={}", deviceId, checkResult.getMessage()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("补偿检查MES任务异常: deviceId={}, error={}", deviceId, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | // 补偿后仍然没有任务记录,说明MES尚未提供可执行任务 |
| | | if (taskInfo == null) { |
| | | Map<String, Object> waitData = new HashMap<>(); |
| | | waitData.put("completed", false); |
| | | waitData.put("waiting", true); |
| | | waitData.put("waitingReason", "noTask"); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("等待MES发送请求(mesSend=1)") |
| | | .data(waitData) |
| | | .build(); |
| | | } |
| | | |
| | | // 获取MES确认超时配置(默认30秒) |
| | | Integer mesConfirmTimeoutMs = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000); |
| | | |
| | | Map<String, Object> data = new HashMap<>(); |
| | | try { |
| | | // 如果尚未向MES汇报(plcReport=1),无需检查确认 |
| | | if (taskInfo.mesConfirmStartTime == null) { |
| | | data.put("completed", false); |
| | | data.put("waiting", true); |
| | | data.put("waitingReason", "waitingReport"); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("大车任务执行中,尚未汇报,无需检查确认") |
| | | .data(data) |
| | | .build(); |
| | | } |
| | | |
| | | // 检查超时 |
| | | long waitTime = System.currentTimeMillis() - taskInfo.mesConfirmStartTime; |
| | | if (waitTime > mesConfirmTimeoutMs) { |
| | | log.warn("MES确认超时: deviceId={}, waitTime={}ms, timeout={}ms", |
| | | deviceId, waitTime, mesConfirmTimeoutMs); |
| | | data.put("completed", false); |
| | | data.put("timeout", true); |
| | | data.put("waitTime", waitTime); |
| | | |
| | | // 超时视为任务失败:清理任务状态并停止监控,避免继续累加等待时间 |
| | | try { |
| | | clearTaskStates(deviceConfig, serializer); |
| | | } catch (Exception e) { |
| | | log.warn("MES确认超时时清空任务状态失败: deviceId={}, error={}", deviceId, e.getMessage()); |
| | | } |
| | | statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.ERROR); |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | currentTasks.remove(deviceConfig.getDeviceId()); |
| | | handleStopTaskMonitor(deviceConfig); |
| | | |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message(String.format("MES确认超时: 等待时间%dms,超时时间%dms", waitTime, mesConfirmTimeoutMs)) |
| | | .data(data) |
| | | .build(); |
| | | } |
| | | |
| | | Object confirmValue = plcDynamicDataService.readPlcField( |
| | | deviceConfig, "mesConfirm", serializer); |
| | | Integer confirm = parseInteger(confirmValue); |
| | |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | |
| | | log.info("MES任务已确认完成: deviceId={}", deviceConfig.getDeviceId()); |
| | | String taskType = taskInfo.isOutbound ? "出片" : "进片"; |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("MES任务已确认完成") |
| | | .message(String.format("%s任务完成:MES已确认(mesConfirm=1),已清空state和汇报字,大车空闲(plcRequest=1),可以等待下次任务", taskType)) |
| | | .data(data) |
| | | .build(); |
| | | } |
| | | |
| | | // 构建详细的等待提示信息 |
| | | String waitMessage = "等待MES确认中(mesConfirm=0)"; |
| | | if (taskInfo.currentStepDesc != null && !taskInfo.currentStepDesc.isEmpty()) { |
| | | waitMessage = taskInfo.currentStepDesc + ";" + waitMessage; |
| | | } else if (taskInfo.mesConfirmStartTime != null) { |
| | | long waitMillis = System.currentTimeMillis() - taskInfo.mesConfirmStartTime; |
| | | waitMessage = String.format("等待MES确认中(mesConfirm=0),已等待%.1f秒", waitMillis / 1000.0); |
| | | } |
| | | |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("等待MES确认中") |
| | | .message(waitMessage) |
| | | .data(data) |
| | | .build(); |
| | | } catch (Exception e) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 设置玻璃为破损状态(state=8) |
| | | * 用于测试场景,模拟玻璃破损 |
| | | */ |
| | | private DevicePlcVO.OperationResult handleMarkBroken(DeviceConfig deviceConfig, |
| | | Map<String, Object> params, |
| | | Map<String, Object> logicParams) { |
| | | String deviceId = deviceConfig.getDeviceId(); |
| | | MesTaskInfo taskInfo = currentTasks.get(deviceId); |
| | | if (taskInfo == null) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("没有正在执行的任务") |
| | | .build(); |
| | | } |
| | | |
| | | // 从参数中获取要标记为破损的玻璃索引(0-based) |
| | | // 支持单个索引或索引列表 |
| | | List<Integer> brokenIndices = new ArrayList<>(); |
| | | Object brokenIndexObj = params.get("glassIndex"); |
| | | if (brokenIndexObj != null) { |
| | | if (brokenIndexObj instanceof List) { |
| | | @SuppressWarnings("unchecked") |
| | | List<Object> indexList = (List<Object>) brokenIndexObj; |
| | | for (Object idx : indexList) { |
| | | Integer index = parseInteger(idx); |
| | | if (index != null && index >= 0 && index < taskInfo.glasses.size()) { |
| | | brokenIndices.add(index); |
| | | } |
| | | } |
| | | } else { |
| | | Integer index = parseInteger(brokenIndexObj); |
| | | if (index != null && index >= 0 && index < taskInfo.glasses.size()) { |
| | | brokenIndices.add(index); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (brokenIndices.isEmpty()) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("未指定有效的玻璃索引") |
| | | .build(); |
| | | } |
| | | |
| | | // 标记为破损 |
| | | if (taskInfo.brokenGlassIndices == null) { |
| | | taskInfo.brokenGlassIndices = new ArrayList<>(); |
| | | } |
| | | for (Integer index : brokenIndices) { |
| | | if (!taskInfo.brokenGlassIndices.contains(index)) { |
| | | taskInfo.brokenGlassIndices.add(index); |
| | | } |
| | | } |
| | | |
| | | // 立即写入PLC的state字段 |
| | | EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); |
| | | if (serializer != null) { |
| | | try { |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | for (Integer index : brokenIndices) { |
| | | String stateField = "state" + (index + 1); |
| | | payload.put(stateField, 8); |
| | | } |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | log.info("已标记玻璃为破损并写入PLC: deviceId={}, brokenIndices={}", |
| | | deviceId, brokenIndices); |
| | | } catch (Exception e) { |
| | | log.error("写入破损状态到PLC失败: deviceId={}, error={}", deviceId, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message(String.format("已标记玻璃为破损: indices=%s", brokenIndices)) |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * 清空任务状态 |
| | | */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * MES任务信息 |
| | | * 单个玻璃的任务信息 |
| | | */ |
| | | private static class MesTaskInfo { |
| | | private static class GlassTaskInfo { |
| | | String glassId; |
| | | Integer startSlot; |
| | | Integer targetSlot; |
| | | Integer startPosition; |
| | | Integer targetPosition; |
| | | Integer width; |
| | | Integer height; |
| | | Integer thickness; |
| | | } |
| | | |
| | | /** |
| | | * MES任务信息(可能包含多个玻璃) |
| | | */ |
| | | private static class MesTaskInfo { |
| | | List<GlassTaskInfo> glasses = new ArrayList<>(); // 多个玻璃信息 |
| | | Integer currentPosition; |
| | | long gotime; |
| | | long cartime; |
| | | Integer workLine; |
| | | long createdTime; |
| | | boolean isOutbound = false; // 是否为出片任务(false=进片,true=出片) |
| | | boolean transferPlcRequestCleared = false; // 是否已清空卧转立plcRequest(避免重复清空) |
| | | List<Integer> brokenGlassIndices = null; // 标记为破损的玻璃索引列表(0-based,用于测试场景) |
| | | Long mesConfirmStartTime = null; // MES确认开始等待的时间(用于超时检测) |
| | | String currentStepDesc = null; // 当前步骤描述(用于显示详细的执行状态) |
| | | } |
| | | |
| | | /** |
| | |
| | | log.debug("未找到TaskExecutionContext,无法通知卧转立扫码设备暂停"); |
| | | } |
| | | } |
| | | |
| | | private void clearDynamicTaskStates(DeviceConfig deviceConfig) { |
| | | if (plcDynamicDataService == null || s7SerializerProvider == null) { |
| | | return; |
| | | } |
| | | try { |
| | | EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); |
| | | if (serializer != null) { |
| | | clearTaskStates(deviceConfig, serializer); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("清空大车state字段失败: deviceId={}, error={}", deviceConfig != null ? deviceConfig.getId() : "null", e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |