| | |
| | | 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()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 构建操作结果 |