package com.jeesite.modules.scheduling; import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType; import com.github.xingshuangs.iot.protocol.s7.service.S7PLC; import com.jeesite.common.config.Global; import com.jeesite.common.lang.ObjectUtils; import com.jeesite.common.lang.StringUtils; import com.jeesite.modules.mes.entity.MesLine; import com.jeesite.modules.mes.entity.MesProduct; import com.jeesite.modules.mes.entity.MesProductRecord; import com.jeesite.modules.mes.entity.MesProductTj; import com.jeesite.modules.mes.resp.BindMaterialResp; import com.jeesite.modules.mes.resp.ParamsResp; import com.jeesite.modules.mes.service.MesLineService; import com.jeesite.modules.mes.service.MesProductRecordService; import com.jeesite.modules.mes.service.MesProductService; import com.jeesite.modules.mes.service.MesProductTjService; import com.jeesite.modules.mes.util.CommonUitl; import com.jeesite.modules.mes.web.MesProductRecordController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * 涂胶 S7 采集与质量握手(OP220,固定工位号)。 *
 * DB1300 点位:
 *   0.0    框架码 String[30]
 *   64.0   扫码反馈信号
 *   64.2   质量校验通过反馈(校验通过 MES 写 true,工位完成后 MES 写 false)
 *   64.4   涂胶数据采集触发
 *   64.5   整体完成信号(true 时提交工位 OK 并回查确认落库,成功后将 64.2 置 false)
 *   66.0   涂胶序号 Word
 *   68.0   涂胶速度 Real
 *   72.0   涂胶长度 Word
 *
 * 配置(application.yml):
 *   mes.tj.enabled       是否启用(默认 false)
 *   mes.tj.plc.ip        PLC IP,默认 10.0.40.100
 *   mes.tj.lineSn        产线编号,默认 XT
 * 
*/ @Configuration @EnableScheduling public class ProductTjScheduling { private static final Logger logger = LoggerFactory.getLogger(ProductTjScheduling.class); private static final String ADDR_SN = "DB1300.0.0"; private static final int SN_LENGTH = 30; private static final String ADDR_SCAN_FEEDBACK = "DB1300.64.0"; private static final String ADDR_QUALITY_PASS = "DB1300.64.2"; private static final String ADDR_GLUE_COLLECT = "DB1300.64.4"; private static final String ADDR_COMPLETE = "DB1300.64.5"; private static final String ADDR_GLUE_SEQ = "DB1300.66.0"; private static final String ADDR_GLUE_SPEED = "DB1300.68.0"; private static final String ADDR_GLUE_LENGTH = "DB1300.72.0"; private static final String DEFAULT_PLC_IP = "10.0.40.100"; private static final String OPRNO = "OP220"; private static final String DEFAULT_LINE_SN = "XT"; @Autowired private MesProductRecordController mesProductRecordController; @Autowired private MesProductService mesProductService; @Autowired private MesLineService mesLineService; @Autowired private MesProductTjService mesProductTjService; @Autowired private MesProductRecordService mesProductRecordService; private final AtomicBoolean running = new AtomicBoolean(false); private volatile String lastKnownSn; private volatile String lastCompletedSn; @Scheduled(fixedRate = 100) public void collect() { String enabled = Global.getConfig("mes.tj.enabled"); if (StringUtils.isBlank(enabled) || !"true".equalsIgnoreCase(enabled.trim())) { return; } runOnce(); } /** * 直接执行一轮(供测试调用) */ public void runOnce() { if (!running.compareAndSet(false, true)) { return; } String ip = getPlcIp(); S7PLC s7PLC = null; try { s7PLC = new S7PLC(EPlcType.S1200, ip); handleScanFeedback(s7PLC); handleGlueCollect(s7PLC); handleComplete(s7PLC); } catch (Exception e) { logger.error("涂胶S7任务失败[{}]:{}", ip, e.getMessage()); } finally { if (s7PLC != null) { try { s7PLC.close(); } catch (Exception ignore) { } } running.set(false); } } /** * 扫码反馈为 true:读框架码 → 质量校验 → 通过则写 DB1300.64.2=true */ private void handleScanFeedback(S7PLC s7PLC) { if (!s7PLC.readBoolean(ADDR_SCAN_FEEDBACK)) { lastCompletedSn = null; return; } String sn = readWorkpieceCode(s7PLC); if (StringUtils.isBlank(sn)) { return; } lastKnownSn = sn; boolean pass = doQualityCheck(sn); if (pass) { s7PLC.writeBoolean(ADDR_QUALITY_PASS, true); logger.info("涂胶质量校验通过 sn={},已写 {}=true", sn, ADDR_QUALITY_PASS); } else { logger.warn("涂胶质量校验未通过 sn={}", sn); } } /** * 涂胶采集触发为 true:读涂胶参数 → 落库 mes_product_tj → 清触发位 */ private void handleGlueCollect(S7PLC s7PLC) { if (!s7PLC.readBoolean(ADDR_GLUE_COLLECT)) { return; } String sn = readWorkpieceCode(s7PLC); if (StringUtils.isBlank(sn)) { sn = lastKnownSn; } if (StringUtils.isBlank(sn)) { logger.error("涂胶数据采集失败:无工件码,保持 {}=true 待重试", ADDR_GLUE_COLLECT); return; } lastKnownSn = sn; int glueSeq = s7PLC.readUInt16(ADDR_GLUE_SEQ); float glueSpeed = s7PLC.readFloat32(ADDR_GLUE_SPEED); int glueLength = s7PLC.readUInt16(ADDR_GLUE_LENGTH); logger.info("涂胶数据采集 sn={} 序号={} 速度={} 长度={}", sn, glueSeq, glueSpeed, glueLength); if (!saveTjData(sn, glueSeq, glueSpeed, glueLength)) { logger.error("涂胶数据保存失败,保持 {}=true 待重试 sn={}", ADDR_GLUE_COLLECT, sn); return; } s7PLC.writeBoolean(ADDR_GLUE_COLLECT, false); logger.info("涂胶数据采集完成 sn={} 序号={},已将 {} 置 false", sn, glueSeq, ADDR_GLUE_COLLECT); } /** * DB1300.64.5 整体完成信号为 true:提交工位 OK → 成功后将 DB1300.64.2 置 false */ private void handleComplete(S7PLC s7PLC) { if (!s7PLC.readBoolean(ADDR_COMPLETE)) { return; } String sn = readWorkpieceCode(s7PLC); if (StringUtils.isBlank(sn)) { sn = lastKnownSn; } if (StringUtils.isBlank(sn)) { logger.error("涂胶工位完成提交失败:无工件码,保持 {}=true 待重试", ADDR_COMPLETE); return; } if (sn.equals(lastCompletedSn)) { return; } if (!submitStationResult(sn, "OK")) { logger.error("涂胶工位结果提交失败,保持信号待重试 sn={}", sn); return; } s7PLC.writeBoolean(ADDR_QUALITY_PASS, false); lastCompletedSn = sn; logger.info("涂胶工位完成 sn={},已提交 OK 并将 {} 置 false", sn, ADDR_QUALITY_PASS); } private boolean saveTjData(String sn, int glueSeq, float glueSpeed, int glueLength) { try { MesProductTj entity = new MesProductTj(); entity.setSn(sn); entity.setOprno(OPRNO); entity.setTjData(String.valueOf(glueSeq)); entity.setTjSpeed(fmt(glueSpeed)); entity.setTjLength(String.valueOf(glueLength)); mesProductTjService.save(entity); return true; } catch (Exception e) { logger.error("涂胶数据保存异常 sn={}:{}", sn, e.getMessage()); return false; } } /** * 直接提交工位结果(供测试 / 手工触发),不读写 PLC 信号。 * 逻辑与 64.5 完成时的 submitStationResult 相同(含回查与强制落库)。 * * @param sn 工件码 * @param result 结果,通常为 OK * @return true=craft=100000 已确认为目标结果 */ public boolean submitStationResultNow(String sn, String result) { if (StringUtils.isBlank(sn)) { logger.error("涂胶工位结果提交跳过:工件码为空"); return false; } return submitStationResult(sn.trim(), StringUtils.isBlank(result) ? "OK" : result.trim()); } /** * 提交工位结果为 OK,并回查 craft=100000 确认已落库。 * updateRecordResult 在单部件工位或缺少总结果行时可能静默跳过,此时改走 updateRecord 强制更新。 */ private boolean submitStationResult(String sn, String result) { String oldOprno = OPRNO + "A"; String lineSn = getLineSn(sn); if (StringUtils.isBlank(lineSn)) { logger.warn("涂胶工位结果提交失败:无法解析产线编号 sn={}", sn); return false; } try { List params = new ArrayList<>(); List bmlists = new ArrayList<>(); mesProductRecordService.updateRecordResult(sn, oldOprno, "100000", "", result, "", params, lineSn, true, bmlists, null); if (isStationMainResult(sn, lineSn, result)) { logger.info("涂胶工位结果已提交 sn={} oprno={} result={}", sn, oldOprno, result); return true; } logger.warn("updateRecordResult 未真正更新 craft=100000,改为 updateRecord 强制提交 sn={} oprno={}", sn, oldOprno); mesProductRecordService.updateRecord(sn, oldOprno, "100000", "", result, "", lineSn); if (!isStationMainResult(sn, lineSn, result)) { logger.error("涂胶工位结果强制提交后仍未落库 sn={} oprno={} lineSn={}", sn, oldOprno, lineSn); return false; } logger.info("涂胶工位结果已强制提交 sn={} oprno={} result={}", sn, oldOprno, result); return true; } catch (Exception e) { logger.error("涂胶工位结果提交异常 sn={}:{}", sn, e.getMessage()); return false; } } /** 回查工位总结果行 craft=100000 是否已为指定结果 */ private boolean isStationMainResult(String sn, String lineSn, String expectContent) { String fOprno = CommonUitl.formatOprno(OPRNO + "A"); MesProductRecord query = new MesProductRecord(); query.setSn(sn); query.setOprno(fOprno); query.setCraft("100000"); query.setLineSn(lineSn); List list = mesProductRecordService.findList(query); if (list == null || list.isEmpty()) { return false; } return expectContent.equals(list.get(0).getContent()); } private boolean doQualityCheck(String sn) { String oldOprno = OPRNO + "A"; String lineSn = getLineSn(sn); if (StringUtils.isBlank(lineSn)) { logger.warn("涂胶质量校验失败:无法解析产线编号 sn={}", sn); return false; } try { String ret = mesProductRecordController.checkQualityCommon(oldOprno, sn, lineSn, ""); if (StringUtils.isBlank(ret) || ret.length() < 5) { return false; } return "UD".equals(ret.substring(3, 5)); } catch (Exception e) { logger.error("涂胶质量校验异常 sn={}:{}", sn, e.getMessage()); return false; } } private String getLineSn(String sn) { String lineSn = Global.getConfig("mes.tj.lineSn"); if (StringUtils.isNotBlank(lineSn)) { return lineSn.trim(); } MesProduct product = mesProductService.findBySn(sn); if (ObjectUtils.isEmpty(product) || StringUtils.isBlank(product.getCate())) { return DEFAULT_LINE_SN; } MesLine query = new MesLine(); query.setProductCate(product.getCate()); query.setStatus("0"); MesLine line = mesLineService.findInfo(query); if (ObjectUtils.isEmpty(line) || StringUtils.isBlank(line.getSn())) { return DEFAULT_LINE_SN; } return line.getSn(); } private String readWorkpieceCode(S7PLC s7PLC) { String raw = s7PLC.readString(ADDR_SN, SN_LENGTH); if (StringUtils.isBlank(raw)) { return null; } StringBuilder sb = new StringBuilder(raw.length()); for (int i = 0; i < raw.length(); i++) { char c = raw.charAt(i); if (c == 0 || c == ' ') { continue; } if (c >= 32 && c < 127) { sb.append(c); } } return sb.toString().trim(); } private String getPlcIp() { String ip = Global.getConfig("mes.tj.plc.ip"); return StringUtils.isBlank(ip) ? DEFAULT_PLC_IP : ip.trim(); } private static String fmt(float val) { return String.format("%.3f", val); } }