| | |
| | | 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.*; |
| | |
| | | switch (operation) { |
| | | case "checkAndProcess": |
| | | case "process": |
| | | return handleCheckAndProcess(deviceConfig, config, logicParams); |
| | | // 这里必须把 params 传进去,以便在多设备任务流程中 |
| | | // 能够通过 _taskContext 将卧转立输出的玻璃ID写入任务上下文 |
| | | return handleCheckAndProcess(deviceConfig, config, logicParams, params); |
| | | case "startMonitor": |
| | | return handleStartMonitor(deviceConfig, config, logicParams); |
| | | case "stopMonitor": |
| | | return handleStopMonitor(deviceConfig); |
| | | case "clearBuffer": |
| | | return handleClearBuffer(deviceConfig); |
| | | case "clearPlc": |
| | | return handleClearPlc(deviceConfig); |
| | | default: |
| | | return buildResult(deviceConfig, operation, false, |
| | | "不支持的操作: " + operation); |
| | |
| | | private DevicePlcVO.OperationResult handleCheckAndProcess( |
| | | DeviceConfig deviceConfig, |
| | | WorkstationLogicConfig config, |
| | | Map<String, Object> logicParams) { |
| | | Map<String, Object> logicParams, |
| | | Map<String, Object> params) { |
| | | |
| | | String deviceId = deviceConfig.getDeviceId(); |
| | | EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); |
| | |
| | | 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. 更新缓冲队列和最后扫码时间 |
| | | updateBuffer(deviceId, recentGlasses); |
| | | lastScanTime.put(deviceId, new AtomicLong(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; |
| | | } |
| | | |
| | | // 7. 从缓冲队列中移除已处理的玻璃 |
| | | removeProcessedGlasses(deviceId, batch); |
| | | // 卧转立批次已成功写入PLC,将本批次玻璃ID写入任务上下文,供大车进片使用 |
| | | try { |
| | | if (params != null) { |
| | | 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; |
| | | List<String> batchGlassIds = batch.stream() |
| | | .map(GlassInfo::getGlassId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList()); |
| | | if (!batchGlassIds.isEmpty()) { |
| | | ctx.getSharedData().put("transferReadyGlassIds", |
| | | new java.util.ArrayList<>(batchGlassIds)); |
| | | log.info("卧转立已输出批次玻璃到任务上下文: deviceId={}, glassIds={}", |
| | | deviceConfig.getId(), batchGlassIds); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.warn("卧转立写入任务上下文transferReadyGlassIds失败: deviceId={}", deviceConfig.getId(), e); |
| | | } |
| | | |
| | | String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s", |
| | | batch.size(), |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(","))); |
| | | // 7. 从缓冲队列中移除已处理的玻璃并更新状态 |
| | | removeProcessedGlasses(deviceId, batch); |
| | | glassInfoService.updateGlassStatus( |
| | | batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()), |
| | | GlassInfo.Status.PROCESSED); |
| | | |
| | | // 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); |
| | | |
| | | // 查询state=1的玻璃记录(已扫码交互完成,等待卧转立处理) |
| | | LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE) |
| | | .ge(GlassInfo::getCreatedTime, twoMinutesAgo) |
| | | wrapper.in(GlassInfo::getStatus, GlassInfo.Status.PENDING, GlassInfo.Status.ACTIVE) |
| | | .eq(GlassInfo::getState, 1) // 只查询state=1的玻璃(已扫码完成) |
| | | .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); |
| | |
| | | |
| | | /** |
| | | * 更新缓冲队列 |
| | | * @return 是否有新的玻璃被加入缓冲(用于判断是否刷新 lastScanTime) |
| | | */ |
| | | private void updateBuffer(String deviceId, List<GlassInfo> newGlasses) { |
| | | private boolean updateBuffer(String deviceId, List<GlassInfo> newGlasses) { |
| | | List<GlassBufferItem> buffer = glassBuffer.computeIfAbsent( |
| | | deviceId, k -> new CopyOnWriteArrayList<>()); |
| | | |
| | |
| | | .map(item -> item.glassInfo.getGlassId()) |
| | | .collect(Collectors.toSet()); |
| | | |
| | | boolean hasNewGlass = false; |
| | | for (GlassInfo glass : newGlasses) { |
| | | if (!existingIds.contains(glass.getGlassId())) { |
| | | buffer.add(new GlassBufferItem(glass, System.currentTimeMillis())); |
| | | hasNewGlass = true; |
| | | log.debug("添加玻璃到缓冲队列: deviceId={}, glassId={}", |
| | | deviceId, glass.getGlassId()); |
| | | } |
| | | } |
| | | return hasNewGlass; |
| | | } |
| | | |
| | | /** |
| | | * 判断是否应该处理批次 |
| | | * 注意:这里只做粗略判断,精确的容量计算(含间隙)在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); |
| | | // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取,直接写入编号,不进行位置映射) |
| | | Object 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; |
| | | inPosition = ctx.getParameters().getExtra() != null |
| | | ? ctx.getParameters().getExtra().get("inPosition") : null; |
| | | } |
| | | } 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) { |
| | |
| | | // 启动监控任务 |
| | | ScheduledFuture<?> future = monitorExecutor.scheduleWithFixedDelay(() -> { |
| | | try { |
| | | handleCheckAndProcess(deviceConfig, config, logicParams); |
| | | // 监控任务不在多设备任务上下文中运行,这里不需要传入 params/_taskContext |
| | | handleCheckAndProcess(deviceConfig, config, logicParams, null); |
| | | } catch (Exception e) { |
| | | log.error("监控任务执行异常: deviceId={}", deviceId, 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()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 构建操作结果 |