统一卧转立扫码、卧转立、大车、大理片笼的定时器逻辑和步骤状态;添加设备拓扑图清除数据、联机状态切换按钮,
| | |
| | | @TableField("description") |
| | | private String description; |
| | | |
| | | @ApiModelProperty(value = "产线编号") |
| | | @TableField("work_line") |
| | | private Integer workLine; |
| | | |
| | | @ApiModelProperty(value = "创建时间") |
| | | @TableField(value = "created_time", fill = FieldFill.INSERT) |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | |
| | | package com.mes.interaction.vehicle.handler; |
| | | |
| | | import com.mes.device.entity.DeviceConfig; |
| | | import com.mes.device.entity.DeviceStatus; |
| | | import com.mes.device.service.DeviceConfigService; |
| | | import com.mes.device.service.DeviceGroupRelationService; |
| | | import com.mes.device.service.DevicePlcOperationService; |
| | | import com.mes.device.service.GlassInfoService; |
| | | import com.mes.device.service.DeviceStatusService; |
| | | import com.mes.device.vo.DeviceGroupVO; |
| | | import com.mes.device.vo.DevicePlcVO; |
| | | import com.mes.interaction.BaseDeviceLogicHandler; |
| | |
| | | |
| | | @Autowired(required = false) |
| | | private DeviceGroupRelationService deviceGroupRelationService; |
| | | |
| | | @Autowired(required = false) |
| | | private DeviceStatusService deviceStatusService; |
| | | |
| | | @Autowired(required = false) |
| | | private PlcDynamicDataService plcDynamicDataService; |
| | |
| | | case "stopTaskMonitor": |
| | | result = handleStopTaskMonitor(deviceConfig); |
| | | break; |
| | | case "setOnlineState": |
| | | result = handleSetOnlineState(deviceConfig, params, logicParams); |
| | | break; |
| | | default: |
| | | log.warn("不支持的操作类型: {}", operation); |
| | | result = DevicePlcVO.OperationResult.builder() |
| | |
| | | * 判断操作是否需要状态检查 |
| | | */ |
| | | private boolean needsStateCheck(String operation) { |
| | | // 所有操作都需要检查状态,除了查询类操作 |
| | | return !"query".equals(operation) && !"status".equals(operation); |
| | | // 所有操作都需要检查状态,除了查询类操作和特定内部检查 |
| | | if ("query".equals(operation) || "status".equals(operation)) { |
| | | return false; |
| | | } |
| | | if ("checkMesConfirm".equals(operation)) { |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | // 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取) |
| | | Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000); |
| | | // 优先使用运行时参数中的glassIntervalMs(从任务参数传入),如果没有则使用设备配置的 |
| | | Integer glassIntervalMs = null; |
| | | if (params.containsKey("glassIntervalMs") && params.get("glassIntervalMs") != null) { |
| | | Object intervalObj = params.get("glassIntervalMs"); |
| | | if (intervalObj instanceof Number) { |
| | | glassIntervalMs = ((Number) intervalObj).intValue(); |
| | | } else if (intervalObj instanceof String) { |
| | | try { |
| | | glassIntervalMs = Integer.parseInt((String) intervalObj); |
| | | } catch (NumberFormatException e) { |
| | | // 忽略 |
| | | } |
| | | } |
| | | } |
| | | if (glassIntervalMs == null) { |
| | | glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000); |
| | | } |
| | | Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm |
| | | Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true); |
| | | Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5); |
| | | |
| | |
| | | Integer positionValue = (Integer) params.get("positionValue"); |
| | | Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed); |
| | | |
| | | List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity, |
| | | List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity, glassGap, |
| | | deviceConfig.getDeviceId()); |
| | | if (plannedGlasses == null) { |
| | | // 玻璃没有长度时返回null表示错误 |
| | |
| | | } |
| | | payload.put("plcGlassCount", plcSlots); |
| | | |
| | | // 写入位置信息 |
| | | // 写入位置信息:PLC侧期望的是 MES 编号(如1001/1002),而不是位置映射后的格子值 |
| | | Integer plcPosition = null; |
| | | if (positionValue != null) { |
| | | payload.put("inPosition", positionValue); |
| | | // 如果调用方直接传了数值,则认为这是MES编号,直接写入 |
| | | plcPosition = positionValue; |
| | | } else if (positionCode != null) { |
| | | // 从位置映射中获取位置值 |
| | | @SuppressWarnings("unchecked") |
| | | Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>()); |
| | | Integer mappedValue = positionMapping.get(positionCode); |
| | | if (mappedValue != null) { |
| | | payload.put("inPosition", mappedValue); |
| | | // 尝试将位置代码解析为数字(例如 "900" -> 900) |
| | | try { |
| | | plcPosition = Integer.parseInt(positionCode.trim()); |
| | | } catch (NumberFormatException ignore) { |
| | | // 非数字编码时,不写入inPosition,由PLC或后续逻辑自行处理 |
| | | } |
| | | } |
| | | if (plcPosition != null) { |
| | | payload.put("inPosition", plcPosition); |
| | | } |
| | | |
| | | // 自动触发请求字 |
| | |
| | | DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields( |
| | | deviceConfig.getId(), payload, operationName); |
| | | |
| | | // 注意:glassIntervalMs 的等待应该在批次之间(在TaskExecutionEngine中处理), |
| | | // 而不是在这里等待,因为这里等待会阻塞大车的正常装玻璃流程 |
| | | // 如果需要在写入后等待,应该在批次之间等待,让大车有时间处理当前批次的玻璃 |
| | | |
| | | // 如果执行成功,更新位置信息到状态,并启动状态监控 |
| | | // 如果执行成功,更新位置信息到状态 |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | VehicleStatus status = statusManager.getOrCreateVehicleStatus( |
| | | deviceConfig.getDeviceId(), deviceConfig.getDeviceName()); |
| | |
| | | VehiclePosition position = new VehiclePosition(positionCode, positionValue); |
| | | status.setCurrentPosition(position); |
| | | } |
| | | |
| | | // 启动自动状态监控,当 state=1 时自动协调卧转立设备 |
| | | startStateMonitoring(deviceConfig, logicParams); |
| | | |
| | | // 仅在“非多设备任务”场景下,才启动大车自身的自动状态监控和 MES 任务监控 |
| | | boolean inMultiDeviceTask = params != null && params.containsKey("_taskContext"); |
| | | if (!inMultiDeviceTask) { |
| | | // 启动自动状态监控,当 state=1 时自动协调卧转立设备 |
| | | startStateMonitoring(deviceConfig, logicParams); |
| | | |
| | | // 从 PLC/MES 创建正式任务并启动监控的逻辑,保留给独立 MES 场景使用 |
| | | // 多设备任务场景下,这部分交由 TaskExecutionEngine 统一编排 |
| | | } |
| | | } |
| | | |
| | | return result; |
| | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 0); |
| | | payload.put("plcReport", 0); |
| | | payload.put("onlineState", Boolean.TRUE); |
| | | |
| | | log.info("大车设备重置: deviceId={}", deviceConfig.getId()); |
| | | |
| | |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | updateDeviceOnlineStatus(deviceConfig, true); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 设置联机状态 |
| | | * @param deviceConfig 设备配置 |
| | | * @param params 参数,可包含 onlineState(1=联机,0=脱机) |
| | | * @param logicParams 逻辑参数 |
| | | * @return 操作结果 |
| | | */ |
| | | private DevicePlcVO.OperationResult handleSetOnlineState( |
| | | DeviceConfig deviceConfig, |
| | | Map<String, Object> params, |
| | | Map<String, Object> logicParams) { |
| | | |
| | | // 从参数中获取联机状态值,默认为true(联机) |
| | | boolean onlineState = true; |
| | | if (params != null && params.containsKey("onlineState")) { |
| | | Object stateObj = params.get("onlineState"); |
| | | if (stateObj instanceof Boolean) { |
| | | onlineState = (Boolean) stateObj; |
| | | } else if (stateObj instanceof Number) { |
| | | onlineState = ((Number) stateObj).intValue() != 0; |
| | | } else if (stateObj instanceof String) { |
| | | try { |
| | | String str = ((String) stateObj).trim(); |
| | | if ("true".equalsIgnoreCase(str)) { |
| | | onlineState = true; |
| | | } else if ("false".equalsIgnoreCase(str)) { |
| | | onlineState = false; |
| | | } else { |
| | | onlineState = Integer.parseInt(str) != 0; |
| | | } |
| | | } catch (NumberFormatException e) { |
| | | log.warn("解析onlineState失败,使用默认值true: deviceId={}, value={}", |
| | | deviceConfig.getId(), stateObj); |
| | | } |
| | | } |
| | | } |
| | | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("onlineState", onlineState); |
| | | |
| | | String stateText = onlineState ? "联机" : "脱机"; |
| | | log.info("大车设备设置联机状态: deviceId={}, onlineState={} ({})", |
| | | deviceConfig.getId(), onlineState, stateText); |
| | | |
| | | DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields( |
| | | deviceConfig.getId(), |
| | | payload, |
| | | "大车设备-设置联机状态(" + stateText + ")" |
| | | ); |
| | | |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | updateDeviceOnlineStatus(deviceConfig, onlineState); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | |
| | | payload.put("plcGlassCount", 0); |
| | | payload.put("plcRequest", 0); |
| | | payload.put("plcReport", 0); |
| | | payload.put("onlineState", Boolean.TRUE); |
| | | |
| | | if (params != null && params.containsKey("positionValue")) { |
| | | payload.put("inPosition", params.get("positionValue")); |
| | |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | stopStateMonitoring(deviceConfig.getDeviceId()); |
| | | updateDeviceOnlineStatus(deviceConfig, true); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | private void updateDeviceOnlineStatus(DeviceConfig deviceConfig, boolean online) { |
| | | if (deviceStatusService == null || deviceConfig == null || deviceConfig.getId() == null) { |
| | | return; |
| | | } |
| | | try { |
| | | String status = online ? DeviceStatus.Status.ONLINE : DeviceStatus.Status.OFFLINE; |
| | | deviceStatusService.updateDeviceOnlineStatus(deviceConfig.getId(), status); |
| | | } catch (Exception e) { |
| | | log.warn("同步设备在线状态到数据库失败: deviceId={}, online={}, error={}", |
| | | deviceConfig.getDeviceId(), online, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) { |
| | |
| | | return "车辆容量(vehicleCapacity)必须大于0"; |
| | | } |
| | | |
| | | Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null); |
| | | if (glassIntervalMs != null && glassIntervalMs < 0) { |
| | | return "玻璃间隔时间(glassIntervalMs)不能为负数"; |
| | | Integer glassGap = getLogicParam(logicParams, "glassGap", null); |
| | | if (glassGap != null && glassGap < 0) { |
| | | return "玻璃间隔(glassGap)不能为负数"; |
| | | } |
| | | |
| | | return null; // 验证通过 |
| | |
| | | public String getDefaultLogicParams() { |
| | | Map<String, Object> defaultParams = new HashMap<>(); |
| | | defaultParams.put("vehicleCapacity", 6000); |
| | | defaultParams.put("glassIntervalMs", 1000); |
| | | defaultParams.put("glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm |
| | | defaultParams.put("autoFeed", true); |
| | | defaultParams.put("maxRetryCount", 5); |
| | | defaultParams.put("defaultGlassLength", 2000); |
| | |
| | | /** |
| | | * 规划玻璃装载 |
| | | * @param source 源玻璃列表 |
| | | * @param vehicleCapacity 车辆容量 |
| | | * @param vehicleCapacity 车辆容量(mm) |
| | | * @param glassGap 玻璃之间的物理间隔(mm),默认200mm |
| | | * @param deviceId 设备ID(用于日志) |
| | | * @return 规划后的玻璃列表,如果玻璃没有长度则返回null(用于测试MES程序) |
| | | */ |
| | | private List<GlassInfo> planGlassLoading(List<GlassInfo> source, |
| | | int vehicleCapacity, |
| | | int glassGap, |
| | | String deviceId) { |
| | | List<GlassInfo> planned = new ArrayList<>(); |
| | | int usedLength = 0; |
| | | int capacity = Math.max(vehicleCapacity, 1); |
| | | int gap = Math.max(glassGap, 0); // 确保间隔不为负数 |
| | | |
| | | for (GlassInfo info : source) { |
| | | Integer glassLength = info.getLength(); |
| | |
| | | int length = glassLength; |
| | | |
| | | if (planned.isEmpty()) { |
| | | // 第一块玻璃,不需要间隙 |
| | | planned.add(info.withLength(length)); |
| | | usedLength = length; |
| | | continue; |
| | | } |
| | | if (usedLength + length <= capacity) { |
| | | |
| | | // 后续玻璃需要考虑间隙:玻璃长度 + 间隙 |
| | | int requiredLength = length + gap; |
| | | if (usedLength + requiredLength <= capacity) { |
| | | planned.add(info.withLength(length)); |
| | | usedLength += length; |
| | | usedLength += requiredLength; // 包含间隙 |
| | | } else { |
| | | // 装不下了,停止添加 |
| | | break; |
| | | } |
| | | } |
| | | |
| | | log.debug("玻璃装载规划: deviceId={}, total={}, planned={}, usedLength={}, capacity={}, glassGap={}", |
| | | deviceId, source.size(), planned.size(), usedLength, capacity, gap); |
| | | |
| | | return planned; |
| | | } |
| | | |
| | |
| | | log.info("已给MES汇报({}任务): deviceId={}, glassId={}", |
| | | taskType, deviceConfig.getDeviceId(), taskInfo.glassId); |
| | | |
| | | // 等待MES确认 |
| | | waitForMesConfirm(deviceConfig, serializer, taskInfo, logicParams); |
| | | |
| | | // 多设备任务场景下,不在这里阻塞等待MES确认,由任务引擎定时调用checkMesConfirm |
| | | } catch (Exception e) { |
| | | log.error("给MES汇报异常: deviceId={}", deviceConfig.getDeviceId(), e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 等待MES确认 |
| | | * 检查MES确认状态(供任务引擎周期性调用) |
| | | * 返回OperationResult.data中的 completed 标志表示是否已确认完成 |
| | | */ |
| | | private void waitForMesConfirm(DeviceConfig deviceConfig, |
| | | EnhancedS7Serializer serializer, |
| | | MesTaskInfo taskInfo, |
| | | Map<String, Object> logicParams) { |
| | | |
| | | public DevicePlcVO.OperationResult checkMesConfirm(DeviceConfig deviceConfig, |
| | | Map<String, Object> logicParams) { |
| | | if (plcDynamicDataService == null || s7SerializerProvider == null) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("PlcDynamicDataService或S7SerializerProvider未注入") |
| | | .build(); |
| | | } |
| | | EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); |
| | | if (serializer == null) { |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("获取PLC序列化器失败") |
| | | .build(); |
| | | } |
| | | |
| | | Map<String, Object> data = new HashMap<>(); |
| | | try { |
| | | // 读取确认字(假设字段名为mesConfirm) |
| | | Integer maxWaitTime = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000); // 默认30秒 |
| | | long startTime = System.currentTimeMillis(); |
| | | |
| | | while (System.currentTimeMillis() - startTime < maxWaitTime) { |
| | | Object confirmValue = plcDynamicDataService.readPlcField( |
| | | deviceConfig, "mesConfirm", serializer); |
| | | Integer confirm = parseInteger(confirmValue); |
| | | |
| | | if (confirm != null && confirm == 1) { |
| | | // MES已确认,清空state和汇报字 |
| | | clearTaskStates(deviceConfig, serializer); |
| | | |
| | | // 任务完成,恢复为空闲状态 |
| | | statusManager.updateVehicleStatus( |
| | | deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | |
| | | // 移除任务 |
| | | currentTasks.remove(deviceConfig.getDeviceId()); |
| | | |
| | | // 停止任务监控 |
| | | handleStopTaskMonitor(deviceConfig); |
| | | |
| | | // 恢复plcRequest=1(可以接收新任务) |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 1); |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | |
| | | log.info("MES任务已完成: deviceId={}, glassId={}", |
| | | deviceConfig.getDeviceId(), taskInfo.glassId); |
| | | return; |
| | | } |
| | | |
| | | Thread.sleep(500); // 等待500ms后重试 |
| | | Object confirmValue = plcDynamicDataService.readPlcField( |
| | | deviceConfig, "mesConfirm", serializer); |
| | | Integer confirm = parseInteger(confirmValue); |
| | | boolean completed = confirm != null && confirm == 1; |
| | | data.put("completed", completed); |
| | | |
| | | if (completed) { |
| | | // MES已确认,清空state和汇报字 |
| | | clearTaskStates(deviceConfig, serializer); |
| | | |
| | | // 任务完成,恢复为空闲状态 |
| | | statusManager.updateVehicleStatus( |
| | | deviceConfig.getDeviceId(), VehicleState.IDLE); |
| | | statusManager.clearVehicleTask(deviceConfig.getDeviceId()); |
| | | |
| | | // 移除任务记录(如果有) |
| | | currentTasks.remove(deviceConfig.getDeviceId()); |
| | | |
| | | // 停止任务监控 |
| | | handleStopTaskMonitor(deviceConfig); |
| | | |
| | | // 恢复plcRequest=1(可以接收新任务) |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | payload.put("plcRequest", 1); |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | |
| | | log.info("MES任务已确认完成: deviceId={}", deviceConfig.getDeviceId()); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("MES任务已确认完成") |
| | | .data(data) |
| | | .build(); |
| | | } |
| | | |
| | | log.warn("等待MES确认超时: deviceId={}, glassId={}", |
| | | deviceConfig.getDeviceId(), taskInfo.glassId); |
| | | |
| | | |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(true) |
| | | .message("等待MES确认中") |
| | | .data(data) |
| | | .build(); |
| | | } catch (Exception e) { |
| | | log.error("等待MES确认异常: deviceId={}", deviceConfig.getDeviceId(), e); |
| | | log.error("检查MES确认状态异常: deviceId={}", deviceConfig.getDeviceId(), e); |
| | | return DevicePlcVO.OperationResult.builder() |
| | | .success(false) |
| | | .message("检查MES确认状态异常: " + e.getMessage()) |
| | | .data(data) |
| | | .build(); |
| | | } |
| | | } |
| | | |
| | |
| | | config.setScanIntervalMs(getLogicParam(logicParams, "scanIntervalMs", 10_000)); |
| | | config.setTransferDelayMs(getLogicParam(logicParams, "transferDelayMs", 30_000)); |
| | | config.setVehicleCapacity(getLogicParam(logicParams, "vehicleCapacity", 6000)); |
| | | config.setGlassGap(getLogicParam(logicParams, "glassGap", 200)); |
| | | return config; |
| | | } |
| | | |
| | |
| | | defaults.put("scanIntervalMs", 10_000); |
| | | defaults.put("transferDelayMs", 30_000); |
| | | defaults.put("vehicleCapacity", 6_000); |
| | | defaults.put("glassGap", 200); |
| | | try { |
| | | return objectMapper.writeValueAsString(defaults); |
| | | } catch (JsonProcessingException e) { |
| | | return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000}"; |
| | | return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000,\"glassGap\":200}"; |
| | | } |
| | | } |
| | | } |
| | |
| | | * 可装载的最大宽度(mm) |
| | | */ |
| | | private Integer vehicleCapacity = 6_000; |
| | | |
| | | /** |
| | | * 玻璃之间的物理间隔(mm) |
| | | */ |
| | | private Integer glassGap = 200; |
| | | } |
| | | |
| | |
| | | if (mesData == null || mesData.isEmpty()) { |
| | | log.error("等待MES写入玻璃信息超时: deviceId={}, timeout={}ms", |
| | | deviceConfig.getId(), config.getScanIntervalMs()); |
| | | // 超时也要清空plcRequest |
| | | clearPlcRequestFields(deviceConfig, serializer); |
| | | return buildResult(deviceConfig, "scanOnce", false, |
| | | String.format("等待MES写入玻璃信息超时(%dms)", config.getScanIntervalMs()), null); |
| | | } |
| | |
| | | // 3. 读取MES回写的玻璃信息 |
| | | String glassId = parseString(mesData.get("mesGlassId")); |
| | | if (!StringUtils.hasText(glassId)) { |
| | | // MES未提供玻璃ID也要清空plcRequest |
| | | clearPlcRequestFields(deviceConfig, serializer); |
| | | return buildResult(deviceConfig, "scanOnce", false, "MES写区未提供玻璃ID", null); |
| | | } |
| | | // 读取MES尺寸数据:mesWidth=表宽,mesHeight=长 |
| | |
| | | } |
| | | glassInfo.setStatus(GlassInfo.Status.PENDING); |
| | | if (workLine != null) { |
| | | glassInfo.setDescription("workLine=" + workLine); |
| | | glassInfo.setWorkLine(workLine); |
| | | } |
| | | Date now = new Date(); |
| | | glassInfo.setCreatedTime(now); |
| | |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | import javax.annotation.PreDestroy; |
| | | |
| | | import javax.annotation.PreDestroy; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.concurrent.*; |
| | |
| | | return handleStopMonitor(deviceConfig); |
| | | case "clearBuffer": |
| | | return handleClearBuffer(deviceConfig); |
| | | case "clearPlc": |
| | | return handleClearPlc(deviceConfig); |
| | | default: |
| | | return buildResult(deviceConfig, operation, false, |
| | | "不支持的操作: " + operation); |
| | |
| | | try { |
| | | // 1. 从数据库查询最近扫码的玻璃信息(最近1分钟内的记录) |
| | | List<GlassInfo> recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams); |
| | | if (recentGlasses.isEmpty()) { |
| | | return buildResult(deviceConfig, "checkAndProcess", true, |
| | | "暂无待处理的玻璃信息"); |
| | | boolean hasNewGlass = false; |
| | | |
| | | if (!recentGlasses.isEmpty()) { |
| | | log.info("查询到最近扫码的玻璃: deviceId={}, count={}", |
| | | deviceId, recentGlasses.size()); |
| | | |
| | | // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间 |
| | | hasNewGlass = updateBuffer(deviceId, recentGlasses); |
| | | if (hasNewGlass) { |
| | | lastScanTime |
| | | .computeIfAbsent(deviceId, k -> new AtomicLong()) |
| | | .set(System.currentTimeMillis()); |
| | | } |
| | | } else { |
| | | log.debug("未查询到最近扫码的玻璃: deviceId={}", deviceId); |
| | | } |
| | | |
| | | log.info("查询到最近扫码的玻璃: deviceId={}, count={}", |
| | | deviceId, recentGlasses.size()); |
| | | |
| | | // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间 |
| | | boolean hasNewGlass = updateBuffer(deviceId, recentGlasses); |
| | | if (hasNewGlass) { |
| | | lastScanTime |
| | | .computeIfAbsent(deviceId, k -> new AtomicLong()) |
| | | .set(System.currentTimeMillis()); |
| | | } |
| | | |
| | | // 3. 检查是否需要立即处理(容量已满或30s内无新玻璃) |
| | | // 3. 检查缓冲队列(即使查询不到新玻璃,缓冲中可能还有待处理的玻璃) |
| | | List<GlassBufferItem> buffer = glassBuffer.get(deviceId); |
| | | if (buffer == null || buffer.isEmpty()) { |
| | | // 缓冲为空且无新玻璃,返回空状态 |
| | | return buildResult(deviceConfig, "checkAndProcess", true, |
| | | "缓冲队列为空"); |
| | | "缓冲队列为空,无待处理玻璃"); |
| | | } |
| | | |
| | | // 4. 判断是否满足处理条件 |
| | | boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config); |
| | | if (!shouldProcess) { |
| | | return buildResult(deviceConfig, "checkAndProcess", true, |
| | | "等待更多玻璃或30s超时"); |
| | | // 未满足处理条件:构造带有等待进度的提示信息,便于前端展示 |
| | | String waitMessage; |
| | | AtomicLong lastTime = lastScanTime.get(deviceId); |
| | | Integer delayMs = config.getTransferDelayMs(); |
| | | if (lastTime != null && delayMs != null && delayMs > 0) { |
| | | long elapsedMs = System.currentTimeMillis() - lastTime.get(); |
| | | if (elapsedMs < 0) { |
| | | elapsedMs = 0; |
| | | } |
| | | long totalMs = delayMs; |
| | | long elapsedSec = elapsedMs / 1000; |
| | | long totalSec = totalMs / 1000; |
| | | waitMessage = String.format("等待更多玻璃或超时触发批次处理 (已等待 %d/%d 秒)", |
| | | elapsedSec, totalSec); |
| | | } else { |
| | | // 没有有效的最后扫码时间或配置,退回到固定提示 |
| | | waitMessage = "等待更多玻璃或30s超时"; |
| | | } |
| | | return buildResult(deviceConfig, "checkAndProcess", true, waitMessage); |
| | | } |
| | | |
| | | // 5. 容量判断和批次组装 |
| | | List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity()); |
| | | // 5. 容量判断和批次组装(考虑玻璃间隙) |
| | | Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm |
| | | List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity(), glassGap); |
| | | if (batch.isEmpty()) { |
| | | return buildResult(deviceConfig, "checkAndProcess", false, |
| | | "无法组装有效批次(容量不足)"); |
| | | } |
| | | |
| | | // 6. 写入PLC |
| | | // 6. 写入PLC(尝试从任务参数中获取卧转立编号) |
| | | DevicePlcVO.OperationResult writeResult = writeBatchToPlc( |
| | | deviceConfig, batch, serializer, logicParams); |
| | | deviceConfig, batch, serializer, logicParams, params); |
| | | |
| | | if (!Boolean.TRUE.equals(writeResult.getSuccess())) { |
| | | return writeResult; |
| | |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()), |
| | | GlassInfo.Status.PROCESSED); |
| | | |
| | | String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s", |
| | | batch.size(), |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(","))); |
| | | // 8. 检查缓冲是否为空,如果为空且无新玻璃,标记为完成 |
| | | List<GlassBufferItem> remainingBuffer = glassBuffer.get(deviceId); |
| | | boolean bufferEmpty = remainingBuffer == null || remainingBuffer.isEmpty(); |
| | | boolean noNewGlass = !hasNewGlass; |
| | | |
| | | String msg; |
| | | if (bufferEmpty && noNewGlass) { |
| | | // 缓冲已清空且无新玻璃,任务完成 |
| | | msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s, 缓冲已清空,任务完成", |
| | | batch.size(), |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(","))); |
| | | } else { |
| | | // 缓冲还有玻璃或可能有新玻璃,继续运行 |
| | | msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s", |
| | | batch.size(), |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(","))); |
| | | } |
| | | return buildResult(deviceConfig, "checkAndProcess", true, msg); |
| | | |
| | | } catch (Exception e) { |
| | |
| | | } |
| | | |
| | | try { |
| | | // 从配置中获取workLine,用于过滤 |
| | | String workLine = getLogicParam(logicParams, "workLine", null); |
| | | // 从配置中获取workLine,用于过滤(配置中是Integer类型) |
| | | Integer workLine = getLogicParam(logicParams, "workLine", null); |
| | | |
| | | // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏) |
| | | Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000); |
| | |
| | | .orderByDesc(GlassInfo::getCreatedTime) |
| | | .last("LIMIT 20"); // 限制查询数量,避免过多 |
| | | |
| | | // 如果配置了workLine,则过滤description |
| | | if (workLine != null && !workLine.isEmpty()) { |
| | | wrapper.like(GlassInfo::getDescription, "workLine=" + workLine); |
| | | // 如果配置了workLine,则过滤work_line字段 |
| | | if (workLine != null) { |
| | | wrapper.eq(GlassInfo::getWorkLine, workLine); |
| | | } |
| | | |
| | | List<GlassInfo> recentGlasses = glassInfoMapper.selectList(wrapper); |
| | |
| | | |
| | | /** |
| | | * 判断是否应该处理批次 |
| | | * 注意:这里只做粗略判断,精确的容量计算(含间隙)在assembleBatch中完成 |
| | | */ |
| | | private boolean shouldProcessBatch(String deviceId, |
| | | List<GlassBufferItem> buffer, |
| | | WorkstationLogicConfig config) { |
| | | // 条件1:缓冲队列已满(达到容量限制) |
| | | // 粗略计算:所有玻璃长度之和(不考虑间隙,因为间隙是动态的) |
| | | int totalLength = buffer.stream() |
| | | .mapToInt(item -> item.glassInfo.getGlassLength() != null ? |
| | | item.glassInfo.getGlassLength() : 0) |
| | | .sum(); |
| | | if (totalLength >= config.getVehicleCapacity()) { |
| | | log.info("缓冲队列容量已满,触发批次处理: deviceId={}, totalLength={}, capacity={}", |
| | | // 粗略判断:如果总长度接近容量(留一些余量给间隙),就触发处理 |
| | | // 精确判断会在assembleBatch中完成 |
| | | if (totalLength >= config.getVehicleCapacity() * 0.8) { // 80%阈值,留余量给间隙 |
| | | log.info("缓冲队列容量接近满载,触发批次处理: deviceId={}, totalLength={}, capacity={}", |
| | | deviceId, totalLength, config.getVehicleCapacity()); |
| | | return true; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 组装批次(容量判断) |
| | | * 组装批次(容量判断,考虑玻璃间隙) |
| | | * @param buffer 缓冲队列 |
| | | * @param vehicleCapacity 车辆容量(mm) |
| | | * @param glassGap 玻璃之间的物理间隔(mm),默认200mm |
| | | * @return 组装好的批次列表 |
| | | */ |
| | | private List<GlassInfo> assembleBatch(List<GlassBufferItem> buffer, |
| | | int vehicleCapacity) { |
| | | int vehicleCapacity, |
| | | int glassGap) { |
| | | List<GlassInfo> batch = new ArrayList<>(); |
| | | int usedLength = 0; |
| | | int gap = Math.max(glassGap, 0); // 确保间隔不为负数 |
| | | |
| | | for (GlassBufferItem item : buffer) { |
| | | GlassInfo glass = item.glassInfo; |
| | | int glassLength = glass.getGlassLength() != null ? |
| | | glass.getGlassLength() : 0; |
| | | |
| | | if (usedLength + glassLength <= vehicleCapacity && batch.size() < 6) { |
| | | batch.add(glass); |
| | | usedLength += glassLength; |
| | | if (glassLength <= 0) { |
| | | continue; // 跳过无效长度的玻璃 |
| | | } |
| | | |
| | | if (batch.isEmpty()) { |
| | | // 第一块玻璃,不需要间隙 |
| | | if (glassLength <= vehicleCapacity && batch.size() < 6) { |
| | | batch.add(glass); |
| | | usedLength = glassLength; |
| | | } else { |
| | | break; // 第一块就装不下 |
| | | } |
| | | } else { |
| | | break; |
| | | // 后续玻璃需要考虑间隙:玻璃长度 + 间隙 |
| | | int requiredLength = glassLength + gap; |
| | | if (usedLength + requiredLength <= vehicleCapacity && batch.size() < 6) { |
| | | batch.add(glass); |
| | | usedLength += requiredLength; // 包含间隙 |
| | | } else { |
| | | break; // 装不下了 |
| | | } |
| | | } |
| | | } |
| | | |
| | | log.debug("批次组装完成: batchSize={}, usedLength={}, capacity={}, glassGap={}", |
| | | batch.size(), usedLength, vehicleCapacity, gap); |
| | | |
| | | return batch; |
| | | } |
| | |
| | | DeviceConfig deviceConfig, |
| | | List<GlassInfo> batch, |
| | | EnhancedS7Serializer serializer, |
| | | Map<String, Object> logicParams) { |
| | | Map<String, Object> logicParams, |
| | | Map<String, Object> params) { |
| | | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | |
| | |
| | | // 写入玻璃数量 |
| | | payload.put("plcGlassCount", count); |
| | | |
| | | // 写入位置信息(如果有配置) |
| | | Integer inPosition = getLogicParam(logicParams, "inPosition", null); |
| | | // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取) |
| | | Integer inPosition = null; |
| | | if (params != null) { |
| | | try { |
| | | Object ctxObj = params.get("_taskContext"); |
| | | if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) { |
| | | com.mes.task.model.TaskExecutionContext ctx = |
| | | (com.mes.task.model.TaskExecutionContext) ctxObj; |
| | | Object positionObj = ctx.getParameters().getExtra() != null |
| | | ? ctx.getParameters().getExtra().get("inPosition") : null; |
| | | if (positionObj instanceof Number) { |
| | | inPosition = ((Number) positionObj).intValue(); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.debug("从任务参数获取卧转立编号失败: deviceId={}", deviceConfig.getId(), e); |
| | | } |
| | | } |
| | | // 如果任务参数中没有,从设备配置中获取 |
| | | if (inPosition == null) { |
| | | inPosition = getLogicParam(logicParams, "inPosition", null); |
| | | } |
| | | if (inPosition != null) { |
| | | payload.put("inPosition", inPosition); |
| | | log.info("写入卧转立编号: deviceId={}, inPosition={}", deviceConfig.getId(), inPosition); |
| | | } else { |
| | | log.debug("未配置卧转立编号,跳过写入: deviceId={}", deviceConfig.getId()); |
| | | } |
| | | |
| | | // 写入请求字(触发大车) |
| | |
| | | |
| | | try { |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | log.info("批次已写入PLC: deviceId={}, glassCount={}", |
| | | deviceConfig.getId(), count); |
| | | log.info("批次已写入PLC: deviceId={}, glassCount={}, inPosition={}", |
| | | deviceConfig.getId(), count, inPosition); |
| | | return buildResult(deviceConfig, "writeBatchToPlc", true, |
| | | "批次写入成功"); |
| | | } catch (Exception e) { |
| | |
| | | log.info("已清空缓冲队列: deviceId={}", deviceId); |
| | | return buildResult(deviceConfig, "clearBuffer", true, "缓冲队列已清空"); |
| | | } |
| | | |
| | | /** |
| | | * 清空PLC相关字段(供测试页面一键清空使用) |
| | | */ |
| | | private DevicePlcVO.OperationResult handleClearPlc(DeviceConfig deviceConfig) { |
| | | try { |
| | | EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); |
| | | if (serializer == null) { |
| | | return buildResult(deviceConfig, "clearPlc", false, "获取PLC序列化器失败"); |
| | | } |
| | | |
| | | Map<String, Object> payload = new HashMap<>(); |
| | | // 根据卧转立主体写入的字段进行清空 |
| | | for (int i = 1; i <= 6; i++) { |
| | | payload.put("plcGlassId" + i, ""); |
| | | } |
| | | payload.put("plcGlassCount", 0); |
| | | payload.put("plcRequest", 0); |
| | | payload.put("inPosition", 0); |
| | | |
| | | plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); |
| | | log.info("卧转立主体清空PLC字段完成: deviceId={}", deviceConfig.getId()); |
| | | return buildResult(deviceConfig, "clearPlc", true, "已清空卧转立主体PLC字段"); |
| | | } catch (Exception e) { |
| | | log.error("卧转立主体清空PLC失败: deviceId={}", deviceConfig.getId(), e); |
| | | return buildResult(deviceConfig, "clearPlc", false, "清空PLC失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 构建操作结果 |
| | |
| | | lowerName.startsWith("plcglassid")) { |
| | | return EDataType.STRING; |
| | | } |
| | | |
| | | // 联机状态等布尔标记 |
| | | if (lowerName.contains("online")) { |
| | | return EDataType.BOOL; |
| | | } |
| | | // 默认返回UINT16 |
| | | return EDataType.UINT16; |
| | | } |
| | |
| | | |
| | | for (DeviceConfig device : devices) { |
| | | String deviceType = device.getDeviceType(); |
| | | log.info("处理设备: deviceId={}, deviceType={}, deviceName={}, WORKSTATION_SCANNER常量={}, equals={}", |
| | | log.debug("处理设备: deviceId={}, deviceType={}, deviceName={}, WORKSTATION_SCANNER常量={}, equals={}", |
| | | device.getId(), deviceType, device.getDeviceName(), |
| | | DeviceConfig.DeviceType.WORKSTATION_SCANNER, |
| | | DeviceConfig.DeviceType.WORKSTATION_SCANNER.equals(deviceType)); |
| | |
| | | || (deviceType != null && (deviceType.contains("扫码") || deviceType.contains("SCANNER"))); |
| | | boolean isLargeGlass = DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType); |
| | | boolean isTransfer = DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceType); |
| | | log.info("设备类型判断: deviceId={}, isLoadVehicle={}, isScanner={}, isLargeGlass={}, isTransfer={}", |
| | | log.debug("设备类型判断: deviceId={}, isLoadVehicle={}, isScanner={}, isLargeGlass={}, isTransfer={}", |
| | | device.getId(), isLoadVehicle, isScanner, isLargeGlass, isTransfer); |
| | | |
| | | // 1. 卧转立扫码设备:启动定时器扫描(每10秒处理一个玻璃ID) |
| | | if (isScanner) { |
| | | log.info("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", |
| | | log.debug("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", |
| | | device.getId(), device.getDeviceType(), device.getDeviceName()); |
| | | TaskStepDetail step = createStepRecord(task, device, currentOrder); |
| | | // 设置步骤为运行状态,并设置开始时间 |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | step.setStartTime(new Date()); |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | |
| | | ScheduledFuture<?> scannerTask = startScannerTimer(task, step, device, context); |
| | | if (scannerTask != null) { |
| | | registerScheduledTask(task.getTaskId(), scannerTask); |
| | | stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,每10秒扫描一次")); |
| | | log.info("扫码设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId()); |
| | | log.debug("扫码设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId()); |
| | | } else { |
| | | log.warn("扫码设备定时器启动失败,glassIds可能为空: deviceId={}, taskId={}, contextParams={}", |
| | | device.getId(), task.getTaskId(), context.getParameters()); |
| | |
| | | |
| | | // 2. 卧转立设备:启动定时器定期检查并处理(中转设备) |
| | | if (isTransfer) { |
| | | log.info("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", |
| | | log.debug("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", |
| | | device.getId(), device.getDeviceType(), device.getDeviceName()); |
| | | TaskStepDetail step = createStepRecord(task, device, currentOrder); |
| | | // 设置步骤为运行状态,并设置开始时间 |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | step.setStartTime(new Date()); |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | |
| | | ScheduledFuture<?> transferTask = startTransferTimer(task, step, device, context); |
| | | if (transferTask != null) { |
| | | registerScheduledTask(task.getTaskId(), transferTask); |
| | | stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,定期检查并处理玻璃批次")); |
| | | log.info("卧转立设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId()); |
| | | log.debug("卧转立设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId()); |
| | | } else { |
| | | log.warn("卧转立设备定时器启动失败: deviceId={}, taskId={}", device.getId(), task.getTaskId()); |
| | | stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败")); |
| | |
| | | boolean isInboundVehicle = currentLoadVehicleIndex == 1; // 第一个大车是进片大车 |
| | | |
| | | TaskStepDetail step = createStepRecord(task, device, currentOrder); |
| | | // 设置步骤为运行状态,并设置开始时间 |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | step.setStartTime(new Date()); |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | |
| | | ScheduledFuture<?> vehicleTask; |
| | | if (isInboundVehicle) { |
| | |
| | | // 4. 大理片笼设备:启动定时器逻辑处理(不涉及PLC交互,只负责逻辑处理) |
| | | if (isLargeGlass) { |
| | | TaskStepDetail step = createStepRecord(task, device, currentOrder); |
| | | // 设置步骤为运行状态,并设置开始时间 |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | step.setStartTime(new Date()); |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | |
| | | ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, step, device, context); |
| | | if (largeGlassTask != null) { |
| | |
| | | // 定时器会在后台持续运行,直到手动停止或超时 |
| | | boolean hasScheduledTasks = !CollectionUtils.isEmpty(taskScheduledTasks.get(task.getTaskId())); |
| | | if (hasScheduledTasks) { |
| | | log.info("任务已启动所有定时器,保持运行状态: taskId={}, scheduledTasksCount={}", |
| | | log.debug("任务已启动所有定时器,保持运行状态: taskId={}, scheduledTasksCount={}", |
| | | task.getTaskId(), taskScheduledTasks.get(task.getTaskId()).size()); |
| | | // 任务保持 RUNNING 状态,定时器在后台运行 |
| | | // 不更新任务状态为 COMPLETED,让任务持续运行 |
| | |
| | | try { |
| | | TaskParameters params = context.getParameters(); |
| | | List<String> glassIds = params.getGlassIds(); |
| | | log.info("卧转立扫码定时器初始化: taskId={}, deviceId={}, glassIds={}, glassIdsSize={}, isEmpty={}", |
| | | log.debug("卧转立扫码定时器初始化: taskId={}, deviceId={}, glassIds={}, glassIdsSize={}, isEmpty={}", |
| | | task.getTaskId(), device.getId(), glassIds, |
| | | glassIds != null ? glassIds.size() : 0, |
| | | CollectionUtils.isEmpty(glassIds)); |
| | |
| | | AtomicInteger successCount = new AtomicInteger(0); |
| | | AtomicInteger failCount = new AtomicInteger(0); |
| | | |
| | | final long CYCLE_INTERVAL_MS = 10_000; // 10秒间隔 |
| | | // 从设备配置中获取扫码间隔,默认10秒 |
| | | Map<String, Object> logicParams = parseLogicParams(device); |
| | | Integer scanIntervalMs = getLogicParam(logicParams, "scanIntervalMs", 10_000); |
| | | |
| | | log.info("启动卧转立扫码定时器: taskId={}, deviceId={}, glassCount={}, interval={}s, glassIds={}", |
| | | task.getTaskId(), device.getId(), glassIds.size(), CYCLE_INTERVAL_MS / 1000, glassIds); |
| | | log.debug("启动卧转立扫码定时器: taskId={}, deviceId={}, glassCount={}, interval={}ms, glassIds={}", |
| | | task.getTaskId(), device.getId(), glassIds.size(), scanIntervalMs, glassIds); |
| | | |
| | | // 启动定时任务 |
| | | ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | if (isTaskCancelled(context)) { |
| | | log.info("任务已取消,停止卧转立扫码定时器: taskId={}, deviceId={}", |
| | | log.debug("任务已取消,停止卧转立扫码定时器: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | return; |
| | | } |
| | | ensureStepRunning(step, task.getTaskId()); |
| | | // 检查是否需要暂停 |
| | | if (shouldPauseScanner(context)) { |
| | | log.debug("卧转立扫码定时器暂停: taskId={}, deviceId={}", task.getTaskId(), device.getId()); |
| | |
| | | // 检查是否还有待处理的玻璃ID |
| | | String glassId = glassIdQueue.poll(); |
| | | if (glassId == null) { |
| | | log.info("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}", |
| | | log.debug("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}", |
| | | task.getTaskId(), device.getId(), processedCount.get(), glassIds.size(), |
| | | successCount.get(), failCount.get()); |
| | | |
| | | // 清空plcRequest和plcGlassId(确保PLC状态清理) |
| | | try { |
| | | DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType()); |
| | | if (handler != null) { |
| | | Map<String, Object> clearParams = new HashMap<>(); |
| | | clearParams.put("_taskContext", context); |
| | | handler.execute(device, "clearPlc", clearParams); |
| | | log.debug("卧转立扫码定时器完成,已清空PLC请求字段: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("卧转立扫码定时器完成时清空PLC失败: taskId={}, deviceId={}, error={}", |
| | | task.getTaskId(), device.getId(), e.getMessage()); |
| | | } |
| | | |
| | | // 若之前未出现失败,再将状态置为完成 |
| | | boolean alreadyFailed = TaskStepDetail.Status.FAILED.name().equals(step.getStatus()); |
| | | if (!alreadyFailed) { |
| | |
| | | } |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | // 扫码设备完成后尝试自动收尾整个任务 |
| | | checkAndCompleteTaskIfDone(step.getTaskId()); |
| | | } |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.COMPLETED, context); |
| | |
| | | } |
| | | |
| | | int currentIndex = processedCount.incrementAndGet(); |
| | | log.info("卧转立扫码定时器处理第{}/{}个玻璃: taskId={}, deviceId={}, glassId={}", |
| | | log.debug("卧转立扫码定时器处理第{}/{}个玻璃: taskId={}, deviceId={}, glassId={}", |
| | | currentIndex, glassIds.size(), task.getTaskId(), device.getId(), glassId); |
| | | |
| | | // 执行单次扫描 |
| | | Map<String, Object> scanParams = new HashMap<>(); |
| | | scanParams.put("glassId", glassId); |
| | | scanParams.put("_taskContext", context); |
| | | log.info("卧转立扫码定时器准备执行: taskId={}, deviceId={}, glassId={}, scanParams={}", |
| | | log.debug("卧转立扫码定时器准备执行: taskId={}, deviceId={}, glassId={}, scanParams={}", |
| | | task.getTaskId(), device.getId(), glassId, scanParams); |
| | | |
| | | DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType()); |
| | | if (handler != null) { |
| | | // 将logicParams合并到scanParams中 |
| | | Map<String, Object> logicParams = parseLogicParams(device); |
| | | // 将logicParams合并到scanParams中(使用已定义的logicParams变量) |
| | | if (logicParams != null && !logicParams.isEmpty()) { |
| | | scanParams.put("_logicParams", logicParams); |
| | | } |
| | | log.info("卧转立扫码定时器调用handler.execute: taskId={}, deviceId={}, glassId={}, operation=scanOnce, scanParamsKeys={}, scanParams={}", |
| | | log.debug("卧转立扫码定时器调用handler.execute: taskId={}, deviceId={}, glassId={}, operation=scanOnce, scanParamsKeys={}, scanParams={}", |
| | | task.getTaskId(), device.getId(), glassId, scanParams.keySet(), scanParams); |
| | | DevicePlcVO.OperationResult result = handler.execute(device, "scanOnce", scanParams); |
| | | log.info("卧转立扫码定时器handler.execute返回: taskId={}, deviceId={}, glassId={}, success={}", |
| | | log.debug("卧转立扫码定时器handler.execute返回: taskId={}, deviceId={}, glassId={}, success={}", |
| | | task.getTaskId(), device.getId(), glassId, result.getSuccess()); |
| | | |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | successCount.incrementAndGet(); |
| | | log.info("卧转立扫码定时器处理成功: taskId={}, deviceId={}, glassId={}", |
| | | log.debug("卧转立扫码定时器处理成功: taskId={}, deviceId={}, glassId={}", |
| | | task.getTaskId(), device.getId(), glassId); |
| | | } else { |
| | | failCount.incrementAndGet(); |
| | |
| | | task.getTaskId(), device.getId(), glassId, result.getMessage()); |
| | | } |
| | | |
| | | // 更新步骤状态 |
| | | updateStepStatus(step, result); |
| | | // 通知步骤更新(让前端实时看到步骤状态) |
| | | // 更新步骤状态(显示进度,保持RUNNING状态直到所有玻璃处理完成) |
| | | updateStepStatusForScanner(step, result, currentIndex, glassIds.size(), |
| | | successCount.get(), failCount.get()); |
| | | // 通知步骤更新(让前端实时看到步骤状态和进度) |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | boolean opSuccess = Boolean.TRUE.equals(result.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | |
| | | log.error("卧转立扫码定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e); |
| | | failCount.incrementAndGet(); |
| | | } |
| | | }, 0, CYCLE_INTERVAL_MS, TimeUnit.MILLISECONDS); |
| | | }, 0, scanIntervalMs, TimeUnit.MILLISECONDS); |
| | | |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.RUNNING, context); |
| | |
| | | Map<String, Object> logicParams = parseLogicParams(device); |
| | | Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs", 5_000); |
| | | |
| | | log.info("启动卧转立设备定时器: taskId={}, deviceId={}, interval={}ms", |
| | | log.debug("启动卧转立设备定时器: taskId={}, deviceId={}, interval={}ms", |
| | | task.getTaskId(), device.getId(), monitorIntervalMs); |
| | | |
| | | // 启动定时任务 |
| | | ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | if (isTaskCancelled(context)) { |
| | | log.info("任务已取消,停止卧转立设备定时器: taskId={}, deviceId={}", |
| | | log.debug("任务已取消,停止卧转立设备定时器: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | return; |
| | | } |
| | | ensureStepRunning(step, task.getTaskId()); |
| | | // 构建参数 |
| | | Map<String, Object> params = new HashMap<>(); |
| | | params.put("_taskContext", context); |
| | |
| | | if (opSuccess) { |
| | | String message = result.getMessage(); |
| | | if (message != null && message.contains("批次已写入PLC")) { |
| | | log.info("卧转立设备定时器执行成功(已写入PLC): taskId={}, deviceId={}, message={}", |
| | | log.debug("卧转立设备定时器执行成功(已写入PLC): taskId={}, deviceId={}, message={}", |
| | | task.getTaskId(), device.getId(), message); |
| | | } else { |
| | | log.debug("卧转立设备定时器等待中: taskId={}, deviceId={}, message={}", |
| | | log.debug("卧转立设备定时器等待中: taskId={}, deviceId={}, message={}", |
| | | task.getTaskId(), device.getId(), message); |
| | | } |
| | | } else { |
| | |
| | | final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次 |
| | | final AtomicInteger lastProcessedCount = new AtomicInteger(0); |
| | | |
| | | log.info("启动进片大车设备定时器: taskId={}, deviceId={}, interval={}s", |
| | | log.debug("启动进片大车设备定时器: taskId={}, deviceId={}, interval={}s", |
| | | task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000); |
| | | |
| | | // 启动定时任务 |
| | | ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | if (isTaskCancelled(context)) { |
| | | log.info("任务已取消,停止进片大车定时器: taskId={}, deviceId={}", |
| | | log.debug("任务已取消,停止进片大车定时器: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | return; |
| | | } |
| | | ensureStepRunning(step, task.getTaskId()); |
| | | // 检查是否有卧转立主体已输出、准备上大车的玻璃信息 |
| | | List<String> readyGlassIds = getTransferReadyGlassIds(context); |
| | | if (CollectionUtils.isEmpty(readyGlassIds)) { |
| | |
| | | return; |
| | | } |
| | | |
| | | log.info("进片大车设备定时器检测到卧转立输出的玻璃信息: taskId={}, deviceId={}, glassCount={}", |
| | | log.debug("进片大车设备定时器检测到卧转立输出的玻璃信息: taskId={}, deviceId={}, glassCount={}", |
| | | task.getTaskId(), device.getId(), currentCount); |
| | | |
| | | // 检查容量 |
| | |
| | | if (logicParams != null && !logicParams.isEmpty()) { |
| | | checkParams.put("_logicParams", logicParams); |
| | | } |
| | | DevicePlcVO.OperationResult result = handler.execute(device, "feedGlass", checkParams); |
| | | // 第一步:写入大车上料请求 |
| | | DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams); |
| | | |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | log.info("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}", |
| | | if (Boolean.TRUE.equals(feedResult.getSuccess())) { |
| | | log.debug("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}", |
| | | task.getTaskId(), device.getId(), readyGlassIds.size()); |
| | | // 将已装载的玻璃ID保存到共享数据中(供大理片笼使用) |
| | | setLoadedGlassIds(context, new ArrayList<>(readyGlassIds)); |
| | |
| | | } else { |
| | | // 装不下,记录容量不足(是否需要影响扫码由工艺再决定) |
| | | log.warn("进片大车设备定时器容量不足: taskId={}, deviceId={}, message={}", |
| | | task.getTaskId(), device.getId(), result.getMessage()); |
| | | task.getTaskId(), device.getId(), feedResult.getMessage()); |
| | | lastProcessedCount.set(currentCount); // 记录当前数量,避免重复检查 |
| | | } |
| | | |
| | | // 更新步骤状态 |
| | | updateStepStatus(step, result); |
| | | boolean opSuccess = Boolean.TRUE.equals(result.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | // 第二步:检查MES确认状态(如果大车处理器支持的话) |
| | | DevicePlcVO.OperationResult mesResult = null; |
| | | try { |
| | | mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap()); |
| | | } catch (Exception e) { |
| | | log.warn("进片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}", |
| | | task.getTaskId(), device.getId(), e.getMessage()); |
| | | } |
| | | |
| | | // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消) |
| | | if (mesResult != null) { |
| | | updateStepStatusForVehicle(step, mesResult); |
| | | boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | } |
| | | } else { |
| | | updateStepStatusForVehicle(step, feedResult); |
| | | boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | |
| | | try { |
| | | final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次 |
| | | |
| | | log.info("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s", |
| | | log.debug("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s", |
| | | task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000); |
| | | |
| | | // 启动定时任务 |
| | | ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | if (isTaskCancelled(context)) { |
| | | log.info("任务已取消,停止出片大车定时器: taskId={}, deviceId={}", |
| | | log.debug("任务已取消,停止出片大车定时器: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | return; |
| | | } |
| | | ensureStepRunning(step, task.getTaskId()); |
| | | // 检查是否有已处理的玻璃信息(从大理片笼来的) |
| | | List<String> processedGlassIds = getProcessedGlassIds(context); |
| | | if (CollectionUtils.isEmpty(processedGlassIds)) { |
| | |
| | | return; |
| | | } |
| | | |
| | | log.info("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}", |
| | | log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}", |
| | | task.getTaskId(), device.getId(), processedGlassIds.size()); |
| | | |
| | | // 执行出片操作 |
| | |
| | | if (logicParams != null && !logicParams.isEmpty()) { |
| | | checkParams.put("_logicParams", logicParams); |
| | | } |
| | | DevicePlcVO.OperationResult result = handler.execute(device, "feedGlass", checkParams); |
| | | // 第一步:写入大车出片请求 |
| | | DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams); |
| | | |
| | | if (Boolean.TRUE.equals(result.getSuccess())) { |
| | | log.info("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}", |
| | | if (Boolean.TRUE.equals(feedResult.getSuccess())) { |
| | | log.debug("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}", |
| | | task.getTaskId(), device.getId(), processedGlassIds.size()); |
| | | // 清空已处理的玻璃ID列表(已处理) |
| | | clearProcessedGlassIds(context); |
| | | } else { |
| | | log.debug("出片大车设备定时器执行失败: taskId={}, deviceId={}, message={}", |
| | | task.getTaskId(), device.getId(), result.getMessage()); |
| | | task.getTaskId(), device.getId(), feedResult.getMessage()); |
| | | } |
| | | |
| | | // 更新步骤状态 |
| | | updateStepStatus(step, result); |
| | | boolean opSuccess = Boolean.TRUE.equals(result.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | // 第二步:检查MES确认状态(如果大车处理器支持的话) |
| | | DevicePlcVO.OperationResult mesResult = null; |
| | | try { |
| | | mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap()); |
| | | } catch (Exception e) { |
| | | log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}", |
| | | task.getTaskId(), device.getId(), e.getMessage()); |
| | | } |
| | | |
| | | // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消) |
| | | if (mesResult != null) { |
| | | updateStepStatusForVehicle(step, mesResult); |
| | | boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | } |
| | | } else { |
| | | updateStepStatusForVehicle(step, feedResult); |
| | | boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess()); |
| | | updateTaskProgress(task, step.getStepOrder(), opSuccess); |
| | | if (!opSuccess) { |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.FAILED, context); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | |
| | | Integer processTimeSeconds = getLogicParam(logicParams, "processTimeSeconds", 30); |
| | | final long PROCESS_TIME_MS = processTimeSeconds * 1000; |
| | | |
| | | log.info("启动大理片笼设备定时器: taskId={}, deviceId={}, processTime={}s", |
| | | log.debug("启动大理片笼设备定时器: taskId={}, deviceId={}, processTime={}s", |
| | | task.getTaskId(), device.getId(), processTimeSeconds); |
| | | |
| | | // 启动定时任务 |
| | | ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | if (isTaskCancelled(context)) { |
| | | log.info("任务已取消,停止大理片笼定时器: taskId={}, deviceId={}", |
| | | log.debug("任务已取消,停止大理片笼定时器: taskId={}, deviceId={}", |
| | | task.getTaskId(), device.getId()); |
| | | return; |
| | | } |
| | | ensureStepRunning(step, task.getTaskId()); |
| | | // 检查是否有已装载的玻璃信息(从进片大车来的) |
| | | List<String> loadedGlassIds = getLoadedGlassIds(context); |
| | | if (CollectionUtils.isEmpty(loadedGlassIds)) { |
| | |
| | | if (processStartTime == null) { |
| | | // 第一次检测到玻璃,记录开始处理时间 |
| | | setProcessStartTime(context, System.currentTimeMillis()); |
| | | log.info("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s", |
| | | log.debug("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s", |
| | | task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds); |
| | | return; |
| | | } |
| | |
| | | } |
| | | |
| | | // 处理时间已到,完成任务汇报 |
| | | log.info("大理片笼设备处理完成: taskId={}, deviceId={}, glassCount={}, processTime={}s", |
| | | log.debug("大理片笼设备处理完成: taskId={}, deviceId={}, glassCount={}, processTime={}s", |
| | | task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds); |
| | | |
| | | // 将已处理的玻璃ID转移到已处理列表(供出片大车使用) |
| | |
| | | step.setErrorMessage(null); |
| | | step.setOutputData(toJson(Collections.singletonMap("glassIds", loadedGlassIds))); |
| | | taskStepDetailMapper.updateById(step); |
| | | // 大理片笼完成后尝试自动收尾整个任务 |
| | | checkAndCompleteTaskIfDone(step.getTaskId()); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("大理片笼设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e); |
| | |
| | | future.cancel(false); |
| | | } |
| | | } |
| | | log.info("已停止任务的所有定时器: taskId={}, count={}", taskId, futures.size()); |
| | | log.debug("已停止任务的所有定时器: taskId={}, count={}", taskId, futures.size()); |
| | | } |
| | | runningTaskContexts.remove(taskId); |
| | | } |
| | |
| | | long timeoutMs = timeoutMinutes * 60 * 1000; |
| | | long deadline = System.currentTimeMillis() + timeoutMs; |
| | | |
| | | log.info("等待定时器任务完成: taskId={}, timeout={}分钟", taskId, timeoutMinutes); |
| | | log.debug("等待定时器任务完成: taskId={}, timeout={}分钟", taskId, timeoutMinutes); |
| | | |
| | | while (System.currentTimeMillis() < deadline) { |
| | | List<ScheduledFuture<?>> futures = taskScheduledTasks.get(taskId); |
| | |
| | | } |
| | | } |
| | | |
| | | log.info("定时器任务等待完成: taskId={}", taskId); |
| | | log.debug("定时器任务等待完成: taskId={}", taskId); |
| | | } |
| | | |
| | | /** |
| | | * 当某个步骤可能完成时,检查任务是否所有步骤都已完成,如果是则自动将任务标记为已完成 |
| | | */ |
| | | private void checkAndCompleteTaskIfDone(String taskId) { |
| | | if (taskId == null) { |
| | | return; |
| | | } |
| | | try { |
| | | MultiDeviceTask task = multiDeviceTaskMapper.selectOne( |
| | | Wrappers.<MultiDeviceTask>lambdaQuery() |
| | | .eq(MultiDeviceTask::getTaskId, taskId) |
| | | ); |
| | | if (task == null) { |
| | | return; |
| | | } |
| | | // 仅在任务仍为RUNNING时才尝试自动收尾 |
| | | if (!MultiDeviceTask.Status.RUNNING.name().equals(task.getStatus())) { |
| | | return; |
| | | } |
| | | |
| | | int totalSteps = task.getTotalSteps() != null ? task.getTotalSteps() : 0; |
| | | if (totalSteps <= 0) { |
| | | return; |
| | | } |
| | | |
| | | int completedSteps = countCompletedSteps(taskId); |
| | | if (completedSteps < totalSteps) { |
| | | return; |
| | | } |
| | | |
| | | // 所有步骤都已完成,收尾任务 |
| | | task.setStatus(MultiDeviceTask.Status.COMPLETED.name()); |
| | | task.setEndTime(new Date()); |
| | | multiDeviceTaskMapper.updateById(task); |
| | | |
| | | // 停止所有定时器 |
| | | stopScheduledTasks(taskId); |
| | | |
| | | // 通知任务完成 |
| | | notificationService.notifyTaskStatus(task); |
| | | |
| | | log.info("所有步骤已完成,自动将任务标记为已完成: taskId={}, totalSteps={}", taskId, totalSteps); |
| | | } catch (Exception e) { |
| | | log.warn("检查并自动完成任务失败: taskId={}", taskId, e); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 确保步骤进入RUNNING状态(仅在第一次真正执行前调用) |
| | | */ |
| | | private void ensureStepRunning(TaskStepDetail step, String taskId) { |
| | | if (step == null) { |
| | | return; |
| | | } |
| | | if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) { |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | if (step.getStartTime() == null) { |
| | | step.setStartTime(new Date()); |
| | | } |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(taskId, step); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 更新扫码设备步骤状态(显示进度,保持RUNNING状态直到所有玻璃处理完成) |
| | | */ |
| | | private void updateStepStatusForScanner(TaskStepDetail step, DevicePlcVO.OperationResult result, |
| | | int currentIndex, int totalCount, |
| | | int successCount, int failCount) { |
| | | if (step == null || result == null) { |
| | | return; |
| | | } |
| | | |
| | | boolean success = Boolean.TRUE.equals(result.getSuccess()); |
| | | |
| | | // 保持RUNNING状态,直到所有玻璃处理完成(在定时器完成时再设置为COMPLETED) |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | |
| | | // 更新时间和耗时,前端可以实时看到执行耗时 |
| | | Date now = new Date(); |
| | | if (step.getStartTime() == null) { |
| | | step.setStartTime(now); |
| | | } |
| | | if (step.getStartTime() != null) { |
| | | step.setDurationMs(now.getTime() - step.getStartTime().getTime()); |
| | | } |
| | | |
| | | // 更新进度信息 |
| | | String progressMessage = String.format("正在处理 %d/%d (成功:%d, 失败:%d)", |
| | | currentIndex, totalCount, successCount, failCount); |
| | | |
| | | if (success) { |
| | | // 成功时显示进度和成功消息 |
| | | String resultMessage = result.getMessage(); |
| | | if (StringUtils.hasText(resultMessage)) { |
| | | step.setSuccessMessage(progressMessage + " - " + resultMessage); |
| | | } else { |
| | | step.setSuccessMessage(progressMessage); |
| | | } |
| | | step.setErrorMessage(null); |
| | | } else { |
| | | // 失败时显示进度和错误消息 |
| | | String errorMessage = result.getMessage(); |
| | | step.setErrorMessage(progressMessage + " - " + (StringUtils.hasText(errorMessage) ? errorMessage : "处理失败")); |
| | | step.setSuccessMessage(null); |
| | | } |
| | | |
| | | step.setOutputData(toJson(result)); |
| | | taskStepDetailMapper.updateById(step); |
| | | } |
| | | |
| | | /** |
| | | * 更新大车设备步骤状态(保持RUNNING,直到手动停止或任务取消;失败时标记为FAILED) |
| | | */ |
| | | private void updateStepStatusForVehicle(TaskStepDetail step, DevicePlcVO.OperationResult result) { |
| | | if (step == null || result == null) { |
| | | return; |
| | | } |
| | | boolean success = Boolean.TRUE.equals(result.getSuccess()); |
| | | boolean completed = false; |
| | | if (result.getData() != null && result.getData().get("completed") != null) { |
| | | Object flag = result.getData().get("completed"); |
| | | if (flag instanceof Boolean) { |
| | | completed = (Boolean) flag; |
| | | } else { |
| | | completed = "true".equalsIgnoreCase(String.valueOf(flag)); |
| | | } |
| | | } |
| | | Date now = new Date(); |
| | | |
| | | // 初始化开始时间 |
| | | if (step.getStartTime() == null) { |
| | | step.setStartTime(now); |
| | | } |
| | | |
| | | if (success && !completed) { |
| | | // 成功但未完成:保持RUNNING状态,仅更新提示信息和耗时 |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | String message = result.getMessage(); |
| | | step.setSuccessMessage(StringUtils.hasText(message) ? message : "大车设备运行中"); |
| | | step.setErrorMessage(null); |
| | | if (step.getStartTime() != null) { |
| | | step.setDurationMs(now.getTime() - step.getStartTime().getTime()); |
| | | } |
| | | } else if (success && completed) { |
| | | // 成功且MES已确认完成:标记为COMPLETED并记录结束时间 |
| | | step.setStatus(TaskStepDetail.Status.COMPLETED.name()); |
| | | String message = result.getMessage(); |
| | | step.setSuccessMessage(StringUtils.hasText(message) ? message : "大车设备任务已完成"); |
| | | step.setErrorMessage(null); |
| | | if (step.getEndTime() == null) { |
| | | step.setEndTime(now); |
| | | } |
| | | if (step.getStartTime() != null && step.getEndTime() != null) { |
| | | step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime()); |
| | | } |
| | | // 尝试自动收尾整个任务 |
| | | checkAndCompleteTaskIfDone(step.getTaskId()); |
| | | } else { |
| | | // 失败:标记为FAILED并记录结束时间 |
| | | step.setStatus(TaskStepDetail.Status.FAILED.name()); |
| | | String message = result.getMessage(); |
| | | step.setErrorMessage(message); |
| | | if (step.getEndTime() == null) { |
| | | step.setEndTime(now); |
| | | } |
| | | if (step.getStartTime() != null && step.getEndTime() != null) { |
| | | step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime()); |
| | | } |
| | | } |
| | | |
| | | step.setOutputData(toJson(result)); |
| | | taskStepDetailMapper.updateById(step); |
| | | } |
| | | |
| | | /** |
| | | * 更新卧转立设备步骤状态(区分等待中和真正完成) |
| | | */ |
| | | private void updateStepStatusForTransfer(TaskStepDetail step, DevicePlcVO.OperationResult result) { |
| | |
| | | boolean success = Boolean.TRUE.equals(result.getSuccess()); |
| | | String message = result.getMessage(); |
| | | |
| | | // 判断是否真正完成(只有写入PLC才算完成) |
| | | boolean isRealCompleted = success && message != null && message.contains("批次已写入PLC"); |
| | | // 判断是否真正完成: |
| | | // 1. 写入PLC成功 |
| | | // 2. 且缓冲已清空(表示所有玻璃已处理完,无新玻璃) |
| | | boolean isRealCompleted = success && message != null |
| | | && message.contains("批次已写入PLC") |
| | | && message.contains("缓冲已清空,任务完成"); |
| | | |
| | | if (isRealCompleted) { |
| | | // 真正完成:设置为完成状态,并设置结束时间 |
| | |
| | | step.setSuccessMessage(message); |
| | | if (step.getEndTime() == null) { |
| | | step.setEndTime(new Date()); |
| | | } |
| | | // 计算耗时 |
| | | if (step.getStartTime() != null && step.getEndTime() != null) { |
| | | step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime()); |
| | | } |
| | | log.debug("卧转立设备步骤已完成: stepId={}, durationMs={}", step.getId(), step.getDurationMs()); |
| | | // 卧转立主体完成后尝试自动收尾整个任务 |
| | | checkAndCompleteTaskIfDone(step.getTaskId()); |
| | | } else if (success && message != null && message.contains("批次已写入PLC")) { |
| | | // 写入PLC成功但缓冲还有玻璃(车满情况),继续运行 |
| | | if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) { |
| | | step.setStatus(TaskStepDetail.Status.RUNNING.name()); |
| | | } |
| | | step.setSuccessMessage(message); |
| | | // 确保开始时间已设置 |
| | | if (step.getStartTime() == null) { |
| | | step.setStartTime(new Date()); |
| | | } |
| | | } else if (success) { |
| | | // 等待中:保持运行状态,只更新消息 |
| | |
| | | step.setErrorMessage(message); |
| | | if (step.getEndTime() == null) { |
| | | step.setEndTime(new Date()); |
| | | } |
| | | // 计算耗时 |
| | | if (step.getStartTime() != null && step.getEndTime() != null) { |
| | | step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime()); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 分批执行大车设备玻璃上料(当玻璃ID数量超过6个且设置了单片间隔时) |
| | | * 分批执行大车设备玻璃上料(当玻璃ID数量超过6个时) |
| | | */ |
| | | private StepResult executeLoadVehicleWithBatches(MultiDeviceTask task, |
| | | DeviceConfig device, |
| | |
| | | TaskExecutionContext context, |
| | | List<Map<String, Object>> stepSummaries) { |
| | | List<String> allGlassIds = context.getParameters().getGlassIds(); |
| | | Integer glassIntervalMs = context.getParameters().getGlassIntervalMs(); |
| | | int batchSize = 6; // 每批最多6个玻璃ID |
| | | |
| | | // 分批处理 |
| | | int totalBatches = (allGlassIds.size() + batchSize - 1) / batchSize; |
| | | log.info("大车设备分批上料: deviceId={}, totalGlassIds={}, batchSize={}, totalBatches={}, glassIntervalMs={}", |
| | | device.getId(), allGlassIds.size(), batchSize, totalBatches, glassIntervalMs); |
| | | log.debug("大车设备分批上料: deviceId={}, totalGlassIds={}, batchSize={}, totalBatches={}", |
| | | device.getId(), allGlassIds.size(), batchSize, totalBatches); |
| | | |
| | | for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { |
| | | int startIndex = batchIndex * batchSize; |
| | |
| | | // 创建临时参数,只包含当前批次的玻璃ID |
| | | TaskParameters batchParams = new TaskParameters(); |
| | | batchParams.setGlassIds(new ArrayList<>(batchGlassIds)); |
| | | batchParams.setGlassIntervalMs(glassIntervalMs); |
| | | batchParams.setPositionCode(context.getParameters().getPositionCode()); |
| | | batchParams.setPositionValue(context.getParameters().getPositionValue()); |
| | | |
| | |
| | | return stepResult; |
| | | } |
| | | |
| | | log.info("大车设备分批上料成功: deviceId={}, batchIndex={}/{}, glassIds={}", |
| | | log.debug("大车设备分批上料成功: deviceId={}, batchIndex={}/{}, glassIds={}", |
| | | device.getId(), batchIndex + 1, totalBatches, batchGlassIds); |
| | | |
| | | // 如果不是最后一批,等待间隔(模拟玻璃每片运动的时间) |
| | | // 这个等待让大车有时间处理当前批次的玻璃,然后再传递下一批 |
| | | if (batchIndex < totalBatches - 1 && glassIntervalMs != null && glassIntervalMs > 0) { |
| | | try { |
| | | log.info("等待单片间隔(模拟玻璃运动时间): glassIntervalMs={}ms, 大车可在此期间继续装玻璃", glassIntervalMs); |
| | | Thread.sleep(glassIntervalMs); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | return StepResult.failure(device.getDeviceName(), "等待单片间隔时被中断"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 更新上下文中的已加载玻璃ID |
| | |
| | | Map<String, Object> params = buildOperationParams(device, context); |
| | | // 将context引用放入params,供设备处理器使用(用于设备协调) |
| | | params.put("_taskContext", context); |
| | | log.info("executeStepWithRetry构建参数: deviceId={}, deviceType={}, operation={}, paramsKeys={}, params={}", |
| | | log.debug("executeStepWithRetry构建参数: deviceId={}, deviceType={}, operation={}, paramsKeys={}, params={}", |
| | | device.getId(), device.getDeviceType(), determineOperation(device, params), params.keySet(), params); |
| | | step.setInputData(toJson(params)); |
| | | taskStepDetailMapper.updateById(step); |
| | |
| | | if (retryAttempt > 0) { |
| | | // 重试前等待 |
| | | long waitTime = retryPolicy.calculateRetryInterval(retryAttempt); |
| | | log.info("步骤执行重试: deviceId={}, operation={}, retryAttempt={}/{}, waitTime={}ms", |
| | | log.debug("步骤执行重试: deviceId={}, operation={}, retryAttempt={}/{}, waitTime={}ms", |
| | | device.getId(), operation, retryAttempt, retryPolicy.getMaxRetryCount(), waitTime); |
| | | Thread.sleep(waitTime); |
| | | |
| | |
| | | try { |
| | | if (retryAttempt > 0) { |
| | | long waitTime = retryPolicy.calculateRetryInterval(retryAttempt); |
| | | log.info("交互步骤执行重试: deviceId={}, retryAttempt={}/{}, waitTime={}ms", |
| | | log.debug("交互步骤执行重试: deviceId={}, retryAttempt={}/{}, waitTime={}ms", |
| | | device.getId(), retryAttempt, retryPolicy.getMaxRetryCount(), waitTime); |
| | | Thread.sleep(waitTime); |
| | | |
| | |
| | | if (taskParams.getPositionValue() != null) { |
| | | params.put("positionValue", taskParams.getPositionValue()); |
| | | } |
| | | // 传递单片间隔配置,如果任务参数中有设置,优先使用任务参数的,否则使用设备配置的 |
| | | if (taskParams.getGlassIntervalMs() != null) { |
| | | params.put("glassIntervalMs", taskParams.getGlassIntervalMs()); |
| | | } |
| | | params.put("triggerRequest", true); |
| | | break; |
| | | case DeviceConfig.DeviceType.LARGE_GLASS: |
| | |
| | | case DeviceConfig.DeviceType.WORKSTATION_SCANNER: |
| | | // 卧转立扫码设备:从任务参数中获取玻璃ID列表,取第一个作为当前要测试的玻璃ID |
| | | // 注意:扫码设备通常通过定时器执行,但如果通过executeStep执行,也需要传递glassId |
| | | log.info("buildOperationParams处理扫码设备: deviceId={}, taskParams.glassIds={}, isEmpty={}", |
| | | log.debug("buildOperationParams处理扫码设备: deviceId={}, taskParams.glassIds={}, isEmpty={}", |
| | | device.getId(), taskParams.getGlassIds(), |
| | | CollectionUtils.isEmpty(taskParams.getGlassIds())); |
| | | if (!CollectionUtils.isEmpty(taskParams.getGlassIds())) { |
| | | params.put("glassId", taskParams.getGlassIds().get(0)); |
| | | params.put("glassIds", new ArrayList<>(taskParams.getGlassIds())); |
| | | log.info("buildOperationParams为扫码设备添加glassId: deviceId={}, glassId={}, glassIdsSize={}", |
| | | log.debug("buildOperationParams为扫码设备添加glassId: deviceId={}, glassId={}, glassIdsSize={}", |
| | | device.getId(), taskParams.getGlassIds().get(0), taskParams.getGlassIds().size()); |
| | | } else { |
| | | log.warn("buildOperationParams扫码设备glassIds为空: deviceId={}, taskParams.glassIds={}, taskParams={}", |
| | |
| | | if (!CollectionUtils.isEmpty(scannerGlassIds)) { |
| | | context.getParameters().setGlassIds(new ArrayList<>(scannerGlassIds)); |
| | | context.setLoadedGlassIds(new ArrayList<>(scannerGlassIds)); |
| | | log.info("卧转立扫码获取到玻璃ID: {}", scannerGlassIds); |
| | | log.debug("卧转立扫码获取到玻璃ID: {}", scannerGlassIds); |
| | | } else { |
| | | log.warn("卧转立扫码未获取到玻璃ID,后续设备可能无法执行"); |
| | | } |
| | |
| | | data.put("durationMs", step.getDurationMs() != null ? step.getDurationMs() : 0); |
| | | data.put("retryCount", step.getRetryCount() != null ? step.getRetryCount() : 0); |
| | | data.put("errorMessage", step.getErrorMessage() != null ? step.getErrorMessage() : ""); |
| | | data.put("successMessage", step.getSuccessMessage() != null ? step.getSuccessMessage() : ""); |
| | | return data; |
| | | } |
| | | |
| | |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="玻璃间隔(秒)"> |
| | | <el-form-item label="玻璃间隙(mm)"> |
| | | <el-input-number |
| | | v-model="glassIntervalSeconds" |
| | | :min="0.1" |
| | | :max="10" |
| | | :step="0.1" |
| | | :precision="1" |
| | | v-model="config.glassGap" |
| | | :min="0" |
| | | :max="1000" |
| | | :step="10" |
| | | style="width: 100%;" |
| | | @change="emitConfigUpdate" |
| | | /> |
| | | <span class="form-tip">玻璃上料间隔时间(秒)</span> |
| | | <span class="form-tip">多块玻璃之间的物理间隔空隙,默认200mm</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | > |
| | | <el-input |
| | | v-model="item.key" |
| | | placeholder="位置代码(如900/901)" |
| | | placeholder="位置代码(如1001/1002)" |
| | | size="small" |
| | | style="width: 150px; margin-right: 10px;" |
| | | @change="handlePositionKeyChange(index)" |
| | |
| | | const config = ref({ |
| | | vehicleCapacity: 6000, |
| | | vehicleSpeed: 1.0, |
| | | glassIntervalMs: 1000, |
| | | glassGap: 200, |
| | | defaultGlassLength: 2000, |
| | | homePosition: 0, |
| | | minRange: 1, |
| | |
| | | syncPositionListFromConfig() |
| | | |
| | | // 时间字段(秒)- 用于前端显示和输入 |
| | | const glassIntervalSeconds = ref(1.0) |
| | | const idleMonitorIntervalSeconds = ref(2.0) |
| | | const taskMonitorIntervalSeconds = ref(1.0) |
| | | const mesConfirmTimeoutSeconds = ref(30) |
| | |
| | | config.value = { |
| | | vehicleCapacity: newVal.vehicleCapacity ?? 6000, |
| | | vehicleSpeed: newVal.vehicleSpeed ?? 1.0, |
| | | glassIntervalMs: newVal.glassIntervalMs ?? 1000, |
| | | glassGap: newVal.glassGap ?? 200, |
| | | defaultGlassLength: newVal.defaultGlassLength ?? 2000, |
| | | homePosition: newVal.homePosition ?? 0, |
| | | minRange: newVal.minRange ?? 1, |
| | |
| | | positionMapping: newVal.positionMapping || {} |
| | | } |
| | | // 将毫秒转换为秒用于显示 |
| | | glassIntervalSeconds.value = (config.value.glassIntervalMs ?? 1000) / 1000 |
| | | idleMonitorIntervalSeconds.value = (config.value.idleMonitorIntervalMs ?? 2000) / 1000 |
| | | taskMonitorIntervalSeconds.value = (config.value.taskMonitorIntervalMs ?? 1000) / 1000 |
| | | mesConfirmTimeoutSeconds.value = (config.value.mesConfirmTimeoutMs ?? 30000) / 1000 |
| | |
| | | }, { immediate: true, deep: true }) |
| | | |
| | | // 监听秒字段变化,转换为毫秒并更新config |
| | | watch(glassIntervalSeconds, (val) => { |
| | | config.value.glassIntervalMs = Math.round(val * 1000) |
| | | emitConfigUpdate() |
| | | }) |
| | | |
| | | watch(idleMonitorIntervalSeconds, (val) => { |
| | | config.value.idleMonitorIntervalMs = Math.round(val * 1000) |
| | | emitConfigUpdate() |
| | |
| | | <div class="workstation-transfer-config"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="扫码间隔(秒)"> |
| | | <el-input-number |
| | | v-model="scanIntervalSeconds" |
| | | :min="1" |
| | | :max="60" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">定时查询最近扫码玻璃的时间间隔,默认10秒</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="缓冲判定时间(秒)"> |
| | | <el-input-number |
| | | v-model="transferDelaySeconds" |
| | |
| | | <span class="form-tip">可装载的最大宽度(毫米),默认6000mm</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="玻璃间隙(mm)"> |
| | | <el-input-number |
| | | v-model="config.glassGap" |
| | | :min="0" |
| | | :max="1000" |
| | | :step="10" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">多块玻璃之间的物理间隔空隙,默认200mm</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="监控间隔(秒)"> |
| | | <el-input-number |
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="位置值(格)"> |
| | | <el-form-item label="卧转立编号"> |
| | | <el-input-number |
| | | v-model="config.inPosition" |
| | | :min="0" |
| | |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | | <span class="form-tip">写入PLC的inPosition值(格子)</span> |
| | | <span class="form-tip">写入PLC的inPosition字段,表示卧转立编号</span> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | |
| | | |
| | | // 配置数据 |
| | | const config = ref({ |
| | | scanIntervalMs: 10000, |
| | | transferDelayMs: 30000, |
| | | vehicleCapacity: 6000, |
| | | glassGap: 200, |
| | | monitorIntervalMs: 10000, |
| | | workLine: null, |
| | | inPosition: null |
| | | }) |
| | | |
| | | // 时间字段(秒)- 用于前端显示和输入 |
| | | const scanIntervalSeconds = ref(10) |
| | | const transferDelaySeconds = ref(30) |
| | | const monitorIntervalSeconds = ref(10) |
| | | |
| | |
| | | watch(() => props.modelValue, (newVal) => { |
| | | if (newVal && Object.keys(newVal).length > 0) { |
| | | config.value = { |
| | | scanIntervalMs: newVal.scanIntervalMs ?? 10000, |
| | | transferDelayMs: newVal.transferDelayMs ?? 30000, |
| | | vehicleCapacity: newVal.vehicleCapacity ?? 6000, |
| | | monitorIntervalMs: newVal.monitorIntervalMs ?? newVal.scanIntervalMs ?? 10000, |
| | | glassGap: newVal.glassGap ?? 200, |
| | | monitorIntervalMs: newVal.monitorIntervalMs ?? 10000, |
| | | workLine: newVal.workLine ?? null, |
| | | inPosition: newVal.inPosition ?? null |
| | | } |
| | | // 将毫秒转换为秒用于显示 |
| | | scanIntervalSeconds.value = (config.value.scanIntervalMs ?? 10000) / 1000 |
| | | transferDelaySeconds.value = (config.value.transferDelayMs ?? 30000) / 1000 |
| | | monitorIntervalSeconds.value = (config.value.monitorIntervalMs ?? 10000) / 1000 |
| | | } |
| | | }, { immediate: true, deep: true }) |
| | | |
| | | // 监听秒字段变化,转换为毫秒并更新config |
| | | watch(scanIntervalSeconds, (val) => { |
| | | config.value.scanIntervalMs = Math.round(val * 1000) |
| | | // 如果monitorIntervalMs未设置,则使用scanIntervalMs |
| | | if (!props.modelValue?.monitorIntervalMs) { |
| | | config.value.monitorIntervalMs = config.value.scanIntervalMs |
| | | monitorIntervalSeconds.value = val |
| | | } |
| | | emit('update:modelValue', { ...config.value }) |
| | | }) |
| | | |
| | | watch(transferDelaySeconds, (val) => { |
| | | config.value.transferDelayMs = Math.round(val * 1000) |
| | | emit('update:modelValue', { ...config.value }) |
| | |
| | | // 监听config其他字段变化,同步到父组件 |
| | | watch(() => [ |
| | | config.value.vehicleCapacity, |
| | | config.value.glassGap, |
| | | config.value.workLine, |
| | | config.value.inPosition |
| | | ], () => { |
| | |
| | | <div class="main-grid"> |
| | | <div class="left-panel"> |
| | | <GroupList @select="handleGroupSelect" /> |
| | | <GroupTopology |
| | | v-if="selectedGroup" |
| | | :group="selectedGroup" |
| | | class="topology-panel" |
| | | /> |
| | | </div> |
| | | <div class="right-panel"> |
| | | <el-tabs v-model="activeTab" type="card" class="workbench-tabs"> |
| | |
| | | <script setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | import GroupList from './components/DeviceGroup/GroupList.vue' |
| | | import GroupTopology from './components/DeviceGroup/GroupTopology.vue' |
| | | import TaskOrchestration from './components/MultiDeviceTest/TaskOrchestration.vue' |
| | | import ExecutionMonitor from './components/MultiDeviceTest/ExecutionMonitor.vue' |
| | | import ResultAnalysis from './components/MultiDeviceTest/ResultAnalysis.vue' |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 24px; |
| | | } |
| | | |
| | | .topology-panel { |
| | | flex: 1; |
| | | min-height: 300px; |
| | | } |
| | | |
| | | .right-panel { |
| | |
| | | <div class="node-name">{{ device.deviceName || device.deviceCode }}</div> |
| | | <div class="node-type">{{ getDeviceTypeLabel(device.deviceType) }}</div> |
| | | <div class="node-status"> |
| | | <el-tag :type="getStatusType(device.status)" size="small"> |
| | | {{ getStatusLabel(device.status) }} |
| | | <el-tag :type="getStatusType(getDeviceStatus(device))" size="small"> |
| | | {{ getStatusLabel(getDeviceStatus(device)) }} |
| | | </el-tag> |
| | | </div> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | size="small" |
| | | text |
| | | @click.stop="clearPlc(device)" |
| | | :loading="clearingDeviceId === (device.deviceId || device.id)" |
| | | > |
| | | 清空 PLC |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | {{ getDeviceTypeLabel(selectedDevice.deviceType) }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="状态"> |
| | | <el-tag :type="getStatusType(selectedDevice.status)"> |
| | | {{ getStatusLabel(selectedDevice.status) }} |
| | | </el-tag> |
| | | <div class="status-control"> |
| | | <el-tag :type="getStatusType(getDeviceStatus(selectedDevice))"> |
| | | {{ getStatusLabel(getDeviceStatus(selectedDevice)) }} |
| | | </el-tag> |
| | | <el-switch |
| | | v-if="isLoadVehicleDevice(selectedDevice)" |
| | | :model-value="Boolean(selectedDevice.onlineState)" |
| | | active-text="联机" |
| | | inactive-text="脱机" |
| | | :loading="togglingDeviceId === (selectedDevice.deviceId || selectedDevice.id)" |
| | | size="small" |
| | | @change="(val) => toggleOnlineState(selectedDevice, val)" |
| | | /> |
| | | <el-button |
| | | size="small" |
| | | text |
| | | @click="clearPlc(selectedDevice)" |
| | | :loading="clearingDeviceId === (selectedDevice.deviceId || selectedDevice.id)" |
| | | > |
| | | 清空 PLC |
| | | </el-button> |
| | | </div> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="PLC IP" v-if="selectedDevice.plcIp"> |
| | | {{ selectedDevice.plcIp }} |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | import { computed, ref, watch, onMounted, onUnmounted } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { |
| | | Refresh, |
| | |
| | | Box, |
| | | Folder |
| | | } from '@element-plus/icons-vue' |
| | | import { deviceGroupApi } from '@/api/device/deviceManagement' |
| | | import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement' |
| | | |
| | | const props = defineProps({ |
| | | group: { |
| | |
| | | const devices = ref([]) |
| | | const layoutMode = ref('horizontal') // 'horizontal' | 'vertical' |
| | | const selectedDevice = ref(null) |
| | | const togglingDeviceId = ref(null) |
| | | const clearingDeviceId = ref(null) |
| | | const refreshIntervalMs = 5000 |
| | | let refreshTimer = null |
| | | |
| | | const fetchDevices = async () => { |
| | | if (!props.group) { |
| | |
| | | ? rawList.data |
| | | : [] |
| | | // 按执行顺序排序 |
| | | devices.value = deviceList.sort((a, b) => { |
| | | const orderA = a.executionOrder || a.order || 0 |
| | | const orderB = b.executionOrder || b.order || 0 |
| | | return orderA - orderB |
| | | }) |
| | | devices.value = deviceList |
| | | .map((device) => normalizeDevice(device)) |
| | | .sort((a, b) => { |
| | | const orderA = a.executionOrder || a.order || 0 |
| | | const orderB = b.executionOrder || b.order || 0 |
| | | return orderA - orderB |
| | | }) |
| | | syncSelectedDevice() |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '加载设备列表失败') |
| | | devices.value = [] |
| | |
| | | |
| | | const handleRefresh = () => { |
| | | fetchDevices() |
| | | } |
| | | |
| | | const stopAutoRefresh = () => { |
| | | if (refreshTimer) { |
| | | clearInterval(refreshTimer) |
| | | refreshTimer = null |
| | | } |
| | | } |
| | | |
| | | const startAutoRefresh = () => { |
| | | stopAutoRefresh() |
| | | if (!props.group) return |
| | | refreshTimer = setInterval(() => { |
| | | fetchDevices() |
| | | }, refreshIntervalMs) |
| | | } |
| | | |
| | | const toggleLayout = () => { |
| | |
| | | () => { |
| | | fetchDevices() |
| | | selectedDevice.value = null |
| | | startAutoRefresh() |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | onMounted(() => { |
| | | startAutoRefresh() |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | stopAutoRefresh() |
| | | }) |
| | | |
| | | // 点击节点选择设备 |
| | | const handleNodeClick = (device) => { |
| | | selectedDevice.value = device |
| | | } |
| | | |
| | | const syncSelectedDevice = () => { |
| | | if (!selectedDevice.value) return |
| | | const deviceId = selectedDevice.value.deviceId || selectedDevice.value.id |
| | | if (!deviceId) return |
| | | const updated = devices.value.find( |
| | | (item) => (item.deviceId || item.id) === deviceId |
| | | ) |
| | | if (updated) { |
| | | selectedDevice.value = updated |
| | | } |
| | | } |
| | | |
| | | const isLoadVehicleDevice = (device) => { |
| | | if (!device || !device.deviceType) return false |
| | | const type = device.deviceType.toUpperCase() |
| | | return type.includes('VEHICLE') || type.includes('大车') |
| | | } |
| | | |
| | | const normalizeDevice = (device) => { |
| | | if (!device) return device |
| | | const normalized = { ...device } |
| | | if (normalized.onlineState !== undefined) { |
| | | normalized.onlineState = toBoolean(normalized.onlineState) |
| | | } else if (normalized.isOnline === true || normalized.isOnline === false) { |
| | | normalized.onlineState = normalized.isOnline |
| | | } else if (normalized.status) { |
| | | normalized.onlineState = String(normalized.status).toUpperCase() === 'ONLINE' |
| | | } |
| | | if (isLoadVehicleDevice(normalized) && normalized.onlineState !== undefined) { |
| | | normalized.status = normalized.onlineState ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | return normalized |
| | | } |
| | | |
| | | const toBoolean = (value) => { |
| | | if (value === true || value === false) return value |
| | | if (typeof value === 'number') return value !== 0 |
| | | const str = String(value).trim().toLowerCase() |
| | | if (str === 'true' || str === '1') return true |
| | | if (str === 'false' || str === '0') return false |
| | | return Boolean(value) |
| | | } |
| | | |
| | | const getDeviceStatus = (device) => { |
| | | if (!device) return 'UNKNOWN' |
| | | if (isLoadVehicleDevice(device) && device.onlineState !== undefined) { |
| | | return device.onlineState ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | if (device.isOnline === true || device.isOnline === false) { |
| | | return device.isOnline ? 'ONLINE' : 'OFFLINE' |
| | | } |
| | | if (device.status) return device.status |
| | | if (device.deviceStatus) return device.deviceStatus |
| | | return 'UNKNOWN' |
| | | } |
| | | |
| | | const toggleOnlineState = async (device, value) => { |
| | | if (!device) return |
| | | const deviceId = device.deviceId || device.id |
| | | if (!deviceId) { |
| | | ElMessage.warning('设备ID不存在,无法设置联机状态') |
| | | return |
| | | } |
| | | try { |
| | | togglingDeviceId.value = deviceId |
| | | await deviceInteractionApi.executeOperation({ |
| | | deviceId, |
| | | operation: 'setOnlineState', |
| | | params: { |
| | | onlineState: value |
| | | } |
| | | }) |
| | | device.onlineState = value |
| | | device.status = value ? 'ONLINE' : 'OFFLINE' |
| | | if (selectedDevice.value && (selectedDevice.value.deviceId === deviceId || selectedDevice.value.id === deviceId)) { |
| | | selectedDevice.value.onlineState = device.onlineState |
| | | selectedDevice.value.status = device.status |
| | | } |
| | | ElMessage.success(`已将 ${device.deviceName || device.deviceCode} 设置为${value ? '联机' : '脱机'}`) |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '设置联机状态失败') |
| | | } finally { |
| | | togglingDeviceId.value = null |
| | | } |
| | | } |
| | | |
| | | const clearPlc = async (device) => { |
| | | if (!device) return |
| | | const deviceId = device.deviceId || device.id |
| | | if (!deviceId) { |
| | | ElMessage.warning('设备ID不存在,无法清空PLC') |
| | | return |
| | | } |
| | | try { |
| | | clearingDeviceId.value = deviceId |
| | | await deviceInteractionApi.executeOperation({ |
| | | deviceId, |
| | | operation: 'clearPlc' |
| | | }) |
| | | ElMessage.success(`已清空 ${device.deviceName || device.deviceCode} 的PLC数据`) |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '清空PLC失败') |
| | | } finally { |
| | | clearingDeviceId.value = null |
| | | } |
| | | } |
| | | |
| | | defineExpose({ |
| | |
| | | justify-content: center; |
| | | } |
| | | |
| | | .node-actions { |
| | | margin-top: 6px; |
| | | display: flex; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .node-order { |
| | | position: absolute; |
| | | top: -8px; |
| | |
| | | justify-content: center; |
| | | } |
| | | |
| | | .status-control { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | /* 水平布局:箭头在节点右侧 */ |
| | | .topology-node-wrapper.layout-horizontal .node-arrow { |
| | | margin-left: 20px; |
| | |
| | | <div class="step-desc" v-if="step.retryCount > 0"> |
| | | 重试次数:{{ step.retryCount }} |
| | | </div> |
| | | <div class="step-desc" v-if="step.successMessage"> |
| | | 提示:{{ step.successMessage }} |
| | | </div> |
| | | <div class="step-desc error-message" v-if="step.errorMessage"> |
| | | <el-icon><Warning /></el-icon> |
| | | 错误:{{ step.errorMessage }} |
| | |
| | | endTime: data.endTime ? new Date(data.endTime) : existingStep.endTime, |
| | | durationMs: data.durationMs !== undefined ? data.durationMs : existingStep.durationMs, |
| | | retryCount: data.retryCount !== undefined ? data.retryCount : existingStep.retryCount, |
| | | successMessage: data.successMessage !== undefined ? data.successMessage : existingStep.successMessage, |
| | | errorMessage: data.errorMessage || existingStep.errorMessage |
| | | } |
| | | } else if (data.stepOrder !== undefined) { |
| | |
| | | v-model="glassIdsInput" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="可选:如果输入玻璃ID,将使用输入的ID进行测试(代替卧转立扫码);如果不输入,将从数据库读取最近扫码的玻璃ID进行测试" |
| | | placeholder="可选:输入玻璃ID,将使用输入的ID进行测试" |
| | | show-word-limit |
| | | :maxlength="5000" |
| | | /> |
| | |
| | | <span v-else>未输入玻璃ID(正常模式:将从数据库读取最近扫码的玻璃ID)</span> |
| | | </div> |
| | | </el-form-item> |
| | | |
| | | <el-divider content-position="left">执行配置</el-divider> |
| | | |
| | | <el-form-item label="单片间隔 (秒)"> |
| | | <el-input-number |
| | | v-model="form.glassIntervalSeconds" |
| | | :min="0" |
| | | :max="60" |
| | | :step="0.1" |
| | | :precision="1" |
| | | placeholder="每个玻璃ID之间的间隔时间" |
| | | /> |
| | | <div class="form-tip">多个玻璃ID时,每个玻璃ID传递之间的间隔时间(秒),用于模拟玻璃每片运动的时间。0表示一次性全部传递</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="执行间隔 (ms)"> |
| | | <el-input-number |
| | | v-model="form.executionInterval" |
| | | :min="100" |
| | | :max="10000" |
| | | :step="100" |
| | | placeholder="设备操作间隔时间" |
| | | /> |
| | | <div class="form-tip">每个设备操作之间的间隔时间(毫秒)</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超时时间 (分钟)"> |
| | | <el-input-number |
| | | v-model="form.timeoutMinutes" |
| | | :min="1" |
| | | :max="60" |
| | | :step="1" |
| | | placeholder="任务超时时间" |
| | | /> |
| | | <div class="form-tip">任务执行的最大超时时间</div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="重试次数"> |
| | | <el-input-number |
| | | v-model="form.retryCount" |
| | | :min="0" |
| | | :max="10" |
| | | :step="1" |
| | | placeholder="失败重试次数" |
| | | /> |
| | | <div class="form-tip">设备操作失败时的最大重试次数</div> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- 设备组拓扑图 --> |
| | | <GroupTopology |
| | | v-if="group" |
| | | :group="group" |
| | | class="topology-section" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { Delete, Promotion } from '@element-plus/icons-vue' |
| | | import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask' |
| | | import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement' |
| | | import GroupTopology from '../DeviceGroup/GroupTopology.vue' |
| | | |
| | | const props = defineProps({ |
| | | group: { |
| | |
| | | |
| | | const emit = defineEmits(['task-started']) |
| | | //配置默认值 |
| | | const form = reactive({ |
| | | glassIntervalSeconds: 10, // 单片间隔,默认10秒 |
| | | executionInterval: 1000, |
| | | timeoutMinutes: 1, |
| | | retryCount: 3 |
| | | }) |
| | | const form = reactive({}) |
| | | |
| | | const formRef = ref(null) |
| | | |
| | |
| | | |
| | | // 构建任务参数 |
| | | // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取 |
| | | // 将秒转换为毫秒传给后端 |
| | | const glassIntervalMs = form.glassIntervalSeconds != null && form.glassIntervalSeconds !== undefined |
| | | ? Math.round(form.glassIntervalSeconds * 1000) |
| | | : 1000 |
| | | const parameters = { |
| | | glassIds: glassIds.value.length > 0 ? glassIds.value : [], |
| | | glassIntervalMs: glassIntervalMs, |
| | | executionInterval: form.executionInterval || 1000 |
| | | } |
| | | |
| | | // 设备特定配置已移除,如有需要可在此扩展 |
| | | if (form.timeoutMinutes) { |
| | | parameters.timeoutMinutes = form.timeoutMinutes |
| | | } |
| | | if (form.retryCount !== null) { |
| | | parameters.retryCount = form.retryCount |
| | | glassIds: glassIds.value.length > 0 ? glassIds.value : [] |
| | | } |
| | | |
| | | // 异步启动任务,立即返回,不阻塞 |
| | |
| | | margin-top: 4px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .topology-section { |
| | | margin-top: 24px; |
| | | } |
| | | </style> |
| | | |