chefz hace 3 semanas
padre
commit
3fe86b3bf4

+ 66 - 0
src/com/mes/ui/ConfigParam.java

@@ -32,6 +32,66 @@ public class ConfigParam {
 	public String lineSn;
 	public String device;
 	public String device2;
+	/** 胶圈次数功能:是=启用,否=停用 */
+	public String sealRingEnable;
+	/** 标漏点检:是=启用,否=停用 */
+	public String leakCheckEnable;
+	/** 标漏程序号,默认 10 */
+	public String leakProgramNo;
+	/** 标漏泄漏率下限,默认 7.8 */
+	public String leakMin;
+	/** 标漏泄漏率上限,默认 8.0 */
+	public String leakMax;
+	/** 每日重置时刻 HH:mm,默认 07:30 */
+	public String leakResetTime;
+
+	public String getSealRingEnable() {
+		return sealRingEnable;
+	}
+
+	public void setSealRingEnable(String sealRingEnable) {
+		this.sealRingEnable = sealRingEnable;
+	}
+
+	public String getLeakCheckEnable() {
+		return leakCheckEnable;
+	}
+
+	public void setLeakCheckEnable(String leakCheckEnable) {
+		this.leakCheckEnable = leakCheckEnable;
+	}
+
+	public String getLeakProgramNo() {
+		return leakProgramNo;
+	}
+
+	public void setLeakProgramNo(String leakProgramNo) {
+		this.leakProgramNo = leakProgramNo;
+	}
+
+	public String getLeakMin() {
+		return leakMin;
+	}
+
+	public void setLeakMin(String leakMin) {
+		this.leakMin = leakMin;
+	}
+
+	public String getLeakMax() {
+		return leakMax;
+	}
+
+	public void setLeakMax(String leakMax) {
+		this.leakMax = leakMax;
+	}
+
+	public String getLeakResetTime() {
+		return leakResetTime;
+	}
+
+	public void setLeakResetTime(String leakResetTime) {
+		this.leakResetTime = leakResetTime;
+	}
 
 	public Integer getId() {
 		return id;
@@ -312,6 +372,12 @@ public class ConfigParam {
 				", labely='" + labely + '\'' +
 				", labelLineSpace='" + labelLineSpace + '\'' +
 				", labelFont='" + labelFont + '\'' +
+				", sealRingEnable='" + sealRingEnable + '\'' +
+				", leakCheckEnable='" + leakCheckEnable + '\'' +
+				", leakProgramNo='" + leakProgramNo + '\'' +
+				", leakMin='" + leakMin + '\'' +
+				", leakMax='" + leakMax + '\'' +
+				", leakResetTime='" + leakResetTime + '\'' +
 				'}';
 	}
 }

+ 57 - 3
src/com/mes/ui/DataUtil.java

@@ -303,13 +303,15 @@ public static JSONObject qmResultData(TestParam testParam) {
                 +"&title="+URLEncoder.encode(titleBase64, "UTF-8")
                 +"&remark="+URLEncoder.encode(testParam.getRemark() != null ? testParam.getRemark() : "", "UTF-8")
                 +"&deviceType="+URLEncoder.encode(testParam.getDeviceType() != null ? testParam.getDeviceType() : "", "UTF-8")
+                +"&craft="+URLEncoder.encode("100000", "UTF-8")
                 // 添加服务器端期望的参数
                 +"&cq="+URLEncoder.encode("", "UTF-8")
                 +"&by="+URLEncoder.encode("", "UTF-8")
                 +"&cs="+URLEncoder.encode("", "UTF-8")
                 +"&leakRate="+URLEncoder.encode("", "UTF-8")
                 +"&leakRateUnit="+URLEncoder.encode("", "UTF-8")
-                +"&type="+URLEncoder.encode("", "UTF-8");
+                +"&type="+URLEncoder.encode("", "UTF-8")
+                +"&recordType="+URLEncoder.encode("QM", "UTF-8");
 
         // 添加sessionid
 //            if (MesClient.sessionid != null && !MesClient.sessionid.isEmpty()) {
@@ -345,6 +347,58 @@ public static JSONObject qmResultData(TestParam testParam) {
 }
 
 
+    /** 上传气密标漏点检结果 */
+    public static JSONObject qmLeakCheckResultData(TestParam testParam) {
+        try {
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductRecord/qmresult";
+            String testPressureBase64 = Base64Utils.getBase64(testParam.getPressure() != null ? testParam.getPressure() : "");
+            String leakValBase64 = Base64Utils.getBase64(testParam.getLeak() != null ? testParam.getLeak() : "");
+            String testTimeBase64 = Base64Utils.getBase64(testParam.getCreateTime() != null ? testParam.getCreateTime() : "");
+            String titleBase64 = Base64Utils.getBase64("气密标漏检测");
+
+            String params = "__ajax=json&oprno=" + URLEncoder.encode(testParam.getOprno() != null ? testParam.getOprno() : "", "UTF-8")
+                    + "&lineSn=" + URLEncoder.encode(testParam.getLineSn() != null ? testParam.getLineSn() : "", "UTF-8")
+                    + "&sn=" + URLEncoder.encode(testParam.getSn() != null ? testParam.getSn() : "LEAK_CHECK", "UTF-8")
+                    + "&workNum=" + URLEncoder.encode(testParam.getUcode() != null ? testParam.getUcode() : "", "UTF-8")
+                    + "&cxm=" + URLEncoder.encode(testParam.getProgramNo() != null ? testParam.getProgramNo() : "", "UTF-8")
+                    + "&result=" + URLEncoder.encode(testParam.getResult() != null ? testParam.getResult() : "", "UTF-8")
+                    + "&testPressure=" + URLEncoder.encode(testPressureBase64, "UTF-8")
+                    + "&testPressureUnit=" + URLEncoder.encode(testParam.getPressureUnit() != null ? testParam.getPressureUnit() : "", "UTF-8")
+                    + "&leakVal=" + URLEncoder.encode(leakValBase64, "UTF-8")
+                    + "&leakValUnit=" + URLEncoder.encode(testParam.getLeakUnit() != null ? testParam.getLeakUnit() : "", "UTF-8")
+                    + "&testTime=" + URLEncoder.encode(testTimeBase64, "UTF-8")
+                    + "&title=" + URLEncoder.encode(titleBase64, "UTF-8")
+                    + "&remark=" + URLEncoder.encode(testParam.getRemark() != null ? testParam.getRemark() : "", "UTF-8")
+                    + "&deviceType=" + URLEncoder.encode(testParam.getDeviceType() != null ? testParam.getDeviceType() : "", "UTF-8")
+                    + "&cq=" + URLEncoder.encode("", "UTF-8")
+                    + "&by=" + URLEncoder.encode("", "UTF-8")
+                    + "&cs=" + URLEncoder.encode("", "UTF-8")
+                    + "&leakRate=" + URLEncoder.encode(testParam.getLeak() != null ? testParam.getLeak() : "", "UTF-8")
+                    + "&leakRateUnit=" + URLEncoder.encode(testParam.getLeakUnit() != null ? testParam.getLeakUnit() : "", "UTF-8")
+                    + "&type=" + URLEncoder.encode("", "UTF-8")
+                    + "&recordType=" + URLEncoder.encode("BL", "UTF-8");
+
+            System.out.println("leakCheck params=" + params);
+            String result = doPost(url, params);
+            System.out.println("leakCheck result=" + result);
+            if (result == null || result.trim().isEmpty() || result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            return JSONObject.parseObject(result);
+        } catch (Exception e) {
+            System.out.println("qmLeakCheckResultData异常: " + e.getMessage());
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
     public static JSONObject getBindMaterail() {
         try{
             String enconding = "UTF-8";
@@ -411,10 +465,10 @@ public static JSONObject qmResultData(TestParam testParam) {
             connection.setReadTimeout(60000);
             connection.setDoOutput(true);
             connection.setDoInput(true);
-            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
             connection.setRequestProperty("Authorization", "Bearer da3efcbf-0845-4fe3-8aba-ee040be542c0");
             os = connection.getOutputStream();
-            os.write(param.getBytes());
+            os.write(param.getBytes("UTF-8"));
             if (connection.getResponseCode() == 200) {
                 is = connection.getInputStream();
                 br = new BufferedReader(new InputStreamReader(is, "UTF-8"));

+ 5 - 0
src/com/mes/ui/LoginFarme.java

@@ -5,6 +5,8 @@ import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
 import com.mes.util.Base64Utils;
 import com.mes.util.HttpUtils;
+import com.mes.util.LeakCheckUploadUtil;
+import com.mes.util.QmUploadUtil;
 
 import javax.swing.*;
 import java.awt.*;
@@ -182,6 +184,9 @@ public class LoginFarme extends JFrame {
                     MesClient.user_menu.setText(user_id);
                     MesClient.welcomeWin.setVisible(false);
                     MesClient.mesClientFrame.setVisible(true);
+                    QmUploadUtil.startRetryTimer();
+                    LeakCheckUploadUtil.startRetryTimer();
+                    MesClient.startDateTimer();
 
                     if(MesClient.jfxPanel == null){
                         String url = "http://"+ MesClient.mes_server_ip+":8980/js/a/mes/mesProductRecord/work?oprno="+MesClient.mes_gw+"&lineSn="+MesClient.mes_line_sn;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1284 - 193
src/com/mes/ui/MesClient.java


+ 1 - 1
src/com/mes/ui/MesRevice.java

@@ -83,7 +83,7 @@ public class MesRevice {
                 MesClient.resetScanA();
                 MesClient.setMenuStatus("结果提交成功,请扫下一件",0);
                 MesClient.scan_type = 1;
-                MesClient.scanBarcode();
+                MesClient.scanBarcodeDialog();
 
             }else{
                 MesClient.setMenuStatus("结果提交失败,请重试",-1);

+ 8 - 3
src/com/mes/util/Base64Utils.java

@@ -2,10 +2,15 @@ package com.mes.util;
 
 import org.apache.commons.codec.binary.Base64;
 
+import java.nio.charset.StandardCharsets;
+
 public class Base64Utils {
+	/** 与服务端 CommonUitl.getBase64Decode(UTF-8) 对齐,避免中文乱码 */
 	public static String getBase64(String str) {
-        byte[] binaryData = str.getBytes();
-        String encodedString = Base64.encodeBase64String(binaryData);
-        return encodedString;
+		if (str == null) {
+			str = "";
+		}
+		byte[] binaryData = str.getBytes(StandardCharsets.UTF_8);
+		return Base64.encodeBase64String(binaryData);
 	}
 }

+ 150 - 0
src/com/mes/util/InnomatecEndFrame.java

@@ -0,0 +1,150 @@
+package com.mes.util;
+
+/**
+ * innomatec 结束帧解析(对齐 mesclient-qm 的 97 长度偏移)。
+ * 串口行可能带 \\r/\\n,禁止对不足长度的串写死 substring(..., 98)。
+ */
+public final class InnomatecEndFrame {
+
+    public final String result;
+    public final String yaLi;
+    public final String yaLiUnit;
+    public final String yaJiang;
+    public final String yaJiangUnit;
+    public final String leakRate;
+    public final String leakRateUnit;
+    public final int rawLen;
+    public final int frameLen;
+
+    private InnomatecEndFrame(String result, String yaLi, String yaLiUnit,
+                              String yaJiang, String yaJiangUnit,
+                              String leakRate, String leakRateUnit,
+                              int rawLen, int frameLen) {
+        this.result = result;
+        this.yaLi = yaLi;
+        this.yaLiUnit = yaLiUnit;
+        this.yaJiang = yaJiang;
+        this.yaJiangUnit = yaJiangUnit;
+        this.leakRate = leakRate;
+        this.leakRateUnit = leakRateUnit;
+        this.rawLen = rawLen;
+        this.frameLen = frameLen;
+    }
+
+    public static boolean looksLikeEndFrame(String raw) {
+        if (raw == null) {
+            return false;
+        }
+        // 过程帧很短;结束帧去换行后约 96,含换行约 97/98
+        if (isProgressLine(raw)) {
+            return false;
+        }
+        String frame = stripEol(raw);
+        int len = frame.length();
+        int rawLen = raw.length();
+        return (rawLen >= 96 && rawLen <= 100) || (len >= 90 && len <= 120);
+    }
+
+    /** 实时过程:时间 压力 压降(Tab 或空格分隔) */
+    public static boolean isProgressLine(String raw) {
+        if (raw == null) {
+            return false;
+        }
+        String line = stripEol(raw).trim();
+        if (line.isEmpty() || line.length() > 40) {
+            return false;
+        }
+        String[] lists = line.split("\\s+");
+        if (lists.length != 3) {
+            return false;
+        }
+        try {
+            Double.parseDouble(lists[0].trim());
+            Double.parseDouble(lists[1].trim());
+            Double.parseDouble(lists[2].trim());
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    public static String[] progressValues(String raw) {
+        return stripEol(raw).trim().split("\\s+");
+    }
+
+    /**
+     * 与 qm 一致:仅整段为 OK/PASS 算合格。
+     * 禁止 contains("OK")——设备 NG 常回 NOK,会被误判成 OK。
+     */
+    public static boolean isPassResult(String result) {
+        if (result == null) {
+            return false;
+        }
+        String r = result.trim().toUpperCase();
+        // 取末段单词,兼容 "xx NOK" / "xx OK"
+        String[] parts = r.split("\\s+");
+        String token = parts[parts.length - 1].replaceAll("[^A-Z]", "");
+        if ("NOK".equals(token) || "NG".equals(token) || "FAIL".equals(token)
+                || r.contains("NOK")) {
+            return false;
+        }
+        return "OK".equals(token) || "PASS".equals(token);
+    }
+
+    public static InnomatecEndFrame parse(String raw) {
+        int rawLen = raw == null ? 0 : raw.length();
+        String frame = stripEol(raw == null ? "" : raw);
+
+        String result;
+        String yaLi = sub(frame, 35, 43);
+        String yaLiUnit = sub(frame, 45, 49);
+        String yaJiang;
+        String yaJiangUnit = sub(frame, 61, 65);
+        String leakRate;
+        String leakRateUnit;
+
+        // 长度>=98 用现场 98 帧偏移,否则用 qm 的 97 帧偏移;单位统一规范为 ml/min
+        if (frame.length() >= 98) {
+            result = sub(frame, 86, 98);
+            yaJiang = sub(frame, 51, 60);
+            leakRate = sub(frame, 67, 76);
+            leakRateUnit = sub(frame, 77, 85);
+        } else {
+            result = sub(frame, 86, 97);
+            yaJiang = sub(frame, 51, 59);
+            leakRate = sub(frame, 67, 75);
+            leakRateUnit = sub(frame, 77, 84);
+        }
+        leakRateUnit = normalizeLeakRateUnit(leakRateUnit);
+
+        return new InnomatecEndFrame(
+                result, yaLi, yaLiUnit, yaJiang, yaJiangUnit, leakRate, leakRateUnit,
+                rawLen, frame.length()
+        );
+    }
+
+    /** 泄漏率单位固定为 ml/min(勿把压降单位 Pa 误当成泄漏率单位) */
+    private static String normalizeLeakRateUnit(String unit) {
+        return "ml/min";
+    }
+
+    private static String stripEol(String s) {
+        int end = s.length();
+        while (end > 0) {
+            char c = s.charAt(end - 1);
+            if (c == '\n' || c == '\r') {
+                end--;
+            } else {
+                break;
+            }
+        }
+        return s.substring(0, end);
+    }
+
+    private static String sub(String s, int from, int to) {
+        if (from >= s.length()) {
+            return "";
+        }
+        return s.substring(from, Math.min(to, s.length())).trim();
+    }
+}

+ 609 - 33
src/com/mes/util/JdbcUtils.java

@@ -108,16 +108,465 @@ public class JdbcUtils {
 		String configRecord2 = "CREATE TABLE if not exists task(\n" +
 				"   id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增ID\n" +
 				"   title VARCHAR(255), -- 任务名称\n" +
-				"   cate VARCHAR(1), -- 任务类型 1=左工位 2=右工位\n" +
+				"   cate VARCHAR(1), -- 任务类型 0=左工位 1=右工位\n" +
 				"   barcode_spec VARCHAR(255), -- 条码固定位\n" +
 				"   barcode_spec_start VARCHAR(20), -- 固定位起始位数\n" +
 				"   barcode_length VARCHAR(50), -- 条码长度\n" +
 				"   content VARCHAR(5000)                 -- 配置内容,json格式 \n" +
 				")";
 		statement.executeUpdate(configRecord2);
+		// 兼容从 qm 拷来的旧库:task 表可能没有 cate 列
+		if (!hasColumn("task", "cate")) {
+			tryAlter("ALTER TABLE task ADD COLUMN cate VARCHAR(1)");
+			try {
+				statement.executeUpdate("UPDATE task SET cate='0' WHERE IFNULL(cate,'')=''");
+			} catch (SQLException ignored) {
+			}
+			log.info("已为 task 表补齐 cate 列(旧任务默认左工位)");
+		}
+
+		try {
+			statement.executeUpdate("ALTER TABLE test_record ADD COLUMN upload_status VARCHAR(1) DEFAULT '0'");
+		} catch (SQLException ignored) {
+		}
+		try {
+			statement.executeUpdate("ALTER TABLE test_record ADD COLUMN upload_retry INT DEFAULT 0");
+		} catch (SQLException ignored) {
+		}
+
+		String appConfig = "CREATE TABLE if not exists app_config("
+				+ "cfg_key VARCHAR(64) PRIMARY KEY,"
+				+ "cfg_value VARCHAR(128)"
+				+ ")";
+		statement.executeUpdate(appConfig);
+		ensureConfigDefault("seal_ring_limit", "200");
+		ensureConfigDefault("seal_ring_count", "0");
+		// 左右工位胶圈独立;若有旧全局值则继承到左侧
+		String legacyLimit = getAppConfig("seal_ring_limit", "200");
+		String legacyCount = getAppConfig("seal_ring_count", "0");
+		ensureConfigDefault("seal_ring_limit_L", legacyLimit);
+		ensureConfigDefault("seal_ring_limit_R", legacyLimit);
+		ensureConfigDefault("seal_ring_count_L", legacyCount);
+		ensureConfigDefault("seal_ring_count_R", "0");
+		ensureConfigDefault("leak_check_ok_time", "");
+		ensureConfigDefault("leak_check_ok_time_L", "");
+		ensureConfigDefault("leak_check_ok_time_R", "");
+
+		String leakCheckRecord = "CREATE TABLE if not exists leak_check_record(\n" +
+				"   id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
+				"   sn VARCHAR(48),\n" +
+				"   ucode VARCHAR(48),\n" +
+				"   line_sn VARCHAR(48),\n" +
+				"   oprno VARCHAR(48),\n" +
+				"   oprno_title VARCHAR(48),\n" +
+				"   create_time DATETIME,\n" +
+				"   pressure VARCHAR(48),\n" +
+				"   pressure_unit VARCHAR(48),\n" +
+				"   leak VARCHAR(48),\n" +
+				"   leak_unit VARCHAR(48),\n" +
+				"   program_no VARCHAR(48),\n" +
+				"   result VARCHAR(48),\n" +
+				"   remark VARCHAR(48),\n" +
+				"   device_type VARCHAR(48),\n" +
+				"   upload_status VARCHAR(1) DEFAULT '0',\n" +
+				"   upload_retry INT DEFAULT 0\n" +
+				")";
+		statement.executeUpdate(leakCheckRecord);
+
+		// 兼容 mesclient-qm 旧库(param1~param5),迁移后可直接读历史
+		ensureQmLegacySchemaCompat();
 
         statement.close();
     }
+
+	/** qm 旧表字段 param1~5 ↔ qm2 的 pressure/leak/program_no */
+	private static void ensureQmLegacySchemaCompat() {
+		migrateParamColumnsToPressure("test_record");
+		migrateParamColumnsToPressure("leak_check_record");
+	}
+
+	private static void migrateParamColumnsToPressure(String table) {
+		try {
+			boolean hasParam1 = hasColumn(table, "param1");
+			boolean hasPressure = hasColumn(table, "pressure");
+			if (!hasPressure) {
+				tryAlter("ALTER TABLE " + table + " ADD COLUMN pressure VARCHAR(48)");
+				tryAlter("ALTER TABLE " + table + " ADD COLUMN pressure_unit VARCHAR(48)");
+				tryAlter("ALTER TABLE " + table + " ADD COLUMN leak VARCHAR(48)");
+				tryAlter("ALTER TABLE " + table + " ADD COLUMN leak_unit VARCHAR(48)");
+				tryAlter("ALTER TABLE " + table + " ADD COLUMN program_no VARCHAR(48)");
+				hasPressure = true;
+			}
+			if (hasParam1 && hasPressure) {
+				Statement st = conn.createStatement();
+				int n = st.executeUpdate(
+						"UPDATE " + table + " SET "
+								+ "pressure=CASE WHEN IFNULL(pressure,'')='' THEN param1 ELSE pressure END, "
+								+ "pressure_unit=CASE WHEN IFNULL(pressure_unit,'')='' THEN param2 ELSE pressure_unit END, "
+								+ "leak=CASE WHEN IFNULL(leak,'')='' THEN param3 ELSE leak END, "
+								+ "leak_unit=CASE WHEN IFNULL(leak_unit,'')='' THEN param4 ELSE leak_unit END, "
+								+ "program_no=CASE WHEN IFNULL(program_no,'')='' THEN param5 ELSE program_no END "
+								+ "WHERE IFNULL(param1,'')<>'' OR IFNULL(param3,'')<>'' OR IFNULL(param5,'')<>''");
+				st.close();
+				if (n > 0) {
+					log.info("已从 qm 旧字段迁移 {} 条到 {}.(pressure/leak)", n, table);
+				}
+			}
+		} catch (SQLException e) {
+			log.error("兼容迁移 {} 失败: {}", table, e.getMessage());
+		}
+	}
+
+	private static void tryAlter(String sql) {
+		try {
+			Statement st = conn.createStatement();
+			st.executeUpdate(sql);
+			st.close();
+		} catch (SQLException ignored) {
+		}
+	}
+
+	private static boolean hasColumn(String table, String column) {
+		try {
+			Statement st = conn.createStatement();
+			ResultSet rs = st.executeQuery("PRAGMA table_info(" + table + ")");
+			while (rs.next()) {
+				if (column.equalsIgnoreCase(rs.getString("name"))) {
+					rs.close();
+					st.close();
+					return true;
+				}
+			}
+			rs.close();
+			st.close();
+		} catch (SQLException e) {
+			log.error("检查列 {}.{} 失败: {}", table, column, e.getMessage());
+		}
+		return false;
+	}
+
+	/** 优先读新字段,没有则回退 qm 旧字段 param* */
+	private static String getCompatCol(ResultSet rs, String modern, String legacy) {
+		try {
+			String v = rs.getString(modern);
+			if (v != null) {
+				return v;
+			}
+		} catch (SQLException ignored) {
+		}
+		try {
+			String v = rs.getString(legacy);
+			return v == null ? "" : v;
+		} catch (SQLException ignored) {
+		}
+		return "";
+	}
+
+	private static void ensureConfigDefault(String key, String defaultValue) {
+		try {
+			if (conn == null || conn.isClosed()) {
+				openConnection();
+			}
+			PreparedStatement ps = conn.prepareStatement("SELECT cfg_value FROM app_config WHERE cfg_key=?");
+			ps.setString(1, key);
+			ResultSet rs = ps.executeQuery();
+			boolean exists = rs.next();
+			rs.close();
+			ps.close();
+			if (!exists) {
+				PreparedStatement ins = conn.prepareStatement("INSERT INTO app_config(cfg_key,cfg_value) VALUES(?,?)");
+				ins.setString(1, key);
+				ins.setString(2, defaultValue);
+				ins.executeUpdate();
+				ins.close();
+			}
+		} catch (SQLException e) {
+			log.error("初始化配置失败 key={}", key, e);
+		}
+	}
+
+	public static String getAppConfig(String key, String defaultValue) {
+		try {
+			if (conn == null || conn.isClosed()) {
+				openConnection();
+			}
+			PreparedStatement ps = conn.prepareStatement("SELECT cfg_value FROM app_config WHERE cfg_key=?");
+			ps.setString(1, key);
+			ResultSet rs = ps.executeQuery();
+			String value = defaultValue;
+			if (rs.next()) {
+				value = rs.getString(1);
+			}
+			rs.close();
+			ps.close();
+			return value == null || value.trim().isEmpty() ? defaultValue : value.trim();
+		} catch (SQLException e) {
+			log.error("读取配置失败 key={}", key, e);
+			return defaultValue;
+		}
+	}
+
+	public static void setAppConfig(String key, String value) {
+		try {
+			if (conn == null || conn.isClosed()) {
+				openConnection();
+			}
+			PreparedStatement ps = conn.prepareStatement(
+					"INSERT OR REPLACE INTO app_config(cfg_key,cfg_value) VALUES(?,?)");
+			ps.setString(1, key);
+			ps.setString(2, value);
+			ps.executeUpdate();
+			ps.close();
+		} catch (SQLException e) {
+			log.error("保存配置失败 key={}", key, e);
+		}
+	}
+
+	private static String sealRingSideKey(String prefix, int side) {
+		return prefix + (side == 1 ? "_R" : "_L");
+	}
+
+	public static int getSealRingLimit(int side) {
+		try {
+			return Integer.parseInt(getAppConfig(sealRingSideKey("seal_ring_limit", side), "200"));
+		} catch (Exception e) {
+			return 200;
+		}
+	}
+
+	public static int getSealRingCount(int side) {
+		try {
+			return Integer.parseInt(getAppConfig(sealRingSideKey("seal_ring_count", side), "0"));
+		} catch (Exception e) {
+			return 0;
+		}
+	}
+
+	public static void setSealRingLimit(int side, int limit) {
+		setAppConfig(sealRingSideKey("seal_ring_limit", side), String.valueOf(Math.max(1, limit)));
+	}
+
+	public static int incrementSealRingCount(int side) {
+		int count = getSealRingCount(side) + 1;
+		setAppConfig(sealRingSideKey("seal_ring_count", side), String.valueOf(count));
+		return count;
+	}
+
+	public static void resetSealRingCount(int side) {
+		setAppConfig(sealRingSideKey("seal_ring_count", side), "0");
+	}
+
+	/** @deprecated 请使用带 side 的重载 */
+	public static int getSealRingLimit() {
+		return getSealRingLimit(0);
+	}
+
+	/** @deprecated 请使用带 side 的重载 */
+	public static int getSealRingCount() {
+		return getSealRingCount(0);
+	}
+
+	/** @deprecated 请使用带 side 的重载 */
+	public static void setSealRingLimit(int limit) {
+		setSealRingLimit(0, limit);
+	}
+
+	/** @deprecated 请使用带 side 的重载 */
+	public static int incrementSealRingCount() {
+		return incrementSealRingCount(0);
+	}
+
+	/** @deprecated 请使用带 side 的重载 */
+	public static void resetSealRingCount() {
+		resetSealRingCount(0);
+	}
+
+	public static int insertLeakCheckRecordReturnId(TestParam testParam) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			String insertSQL = "INSERT INTO leak_check_record (oprno,oprno_title,ucode, sn, line_sn, create_time, pressure, pressure_unit, leak, leak_unit, program_no, result, remark, device_type, upload_status, upload_retry)" +
+					"VALUES('" + testParam.getOprno() + "','" + testParam.getOprnoTitle() + "','" + testParam.getUcode() + "', '" + testParam.getSn() + "', '" + testParam.getLineSn() + "', '" + testParam.getCreateTime() + "', '" + testParam.getPressure() + "', '"
+					+ testParam.getPressureUnit() + "','" + testParam.getLeak() + "','" + testParam.getLeakUnit() + "','" + testParam.getProgramNo() + "','" + testParam.getResult() + "','" + testParam.getRemark() + "','" + testParam.getDeviceType() + "','0',0)";
+			statement.executeUpdate(insertSQL);
+			ResultSet rs = statement.executeQuery("SELECT last_insert_rowid()");
+			int id = -1;
+			if (rs.next()) {
+				id = rs.getInt(1);
+			}
+			rs.close();
+			statement.close();
+			log.info("向leak_check_record插入成功, id={}", id);
+			return id;
+		} catch (SQLException e) {
+			log.error("向leak_check_record插入失败: {}", e.getMessage());
+			e.printStackTrace();
+		}
+		return -1;
+	}
+
+	public static void updateLeakCheckUploadStatus(int id, String status) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("UPDATE leak_check_record SET upload_status='" + status + "' WHERE id=" + id);
+			statement.close();
+		} catch (SQLException e) {
+			log.error("更新leak_check upload_status失败: {}", e.getMessage());
+		}
+	}
+
+	public static void incrementLeakCheckUploadRetry(int id) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("UPDATE leak_check_record SET upload_retry=IFNULL(upload_retry,0)+1 WHERE id=" + id);
+			statement.close();
+		} catch (SQLException e) {
+			log.error("更新leak_check upload_retry失败: {}", e.getMessage());
+		}
+	}
+
+	public static int getLeakCheckUploadRetry(int id) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			ResultSet rs = statement.executeQuery("SELECT upload_retry FROM leak_check_record WHERE id=" + id);
+			if (rs.next()) {
+				return rs.getInt(1);
+			}
+			rs.close();
+			statement.close();
+		} catch (SQLException e) {
+			log.error("查询leak_check upload_retry失败: {}", e.getMessage());
+		}
+		return 0;
+	}
+
+	public static List<TestParam> getPendingLeakCheckUploadRecords(int limit) {
+		List<TestParam> list = new ArrayList<>();
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			ResultSet rs = statement.executeQuery(
+					"SELECT * FROM leak_check_record WHERE IFNULL(upload_status,'0') != '1' AND IFNULL(upload_retry,0) < 20 ORDER BY id ASC LIMIT " + limit);
+			while (rs.next()) {
+				list.add(mapLeakTestParam(rs));
+			}
+			rs.close();
+			statement.close();
+		} catch (SQLException e) {
+			log.error("查询待上传标漏记录失败: {}", e.getMessage());
+		}
+		return list;
+	}
+
+	private static TestParam mapLeakTestParam(ResultSet rs) throws SQLException {
+		TestParam testParam = new TestParam();
+		testParam.setId(rs.getInt("id"));
+		testParam.setSn(rs.getString("sn"));
+		testParam.setOprno(rs.getString("oprno"));
+		testParam.setOprnoTitle(rs.getString("oprno_title"));
+		testParam.setUcode(rs.getString("ucode"));
+		testParam.setLineSn(rs.getString("line_sn"));
+		testParam.setCreateTime(rs.getString("create_time"));
+		testParam.setPressure(getCompatCol(rs, "pressure", "param1"));
+		testParam.setPressureUnit(getCompatCol(rs, "pressure_unit", "param2"));
+		testParam.setLeak(getCompatCol(rs, "leak", "param3"));
+		testParam.setLeakUnit(getCompatCol(rs, "leak_unit", "param4"));
+		testParam.setProgramNo(getCompatCol(rs, "program_no", "param5"));
+		testParam.setResult(rs.getString("result"));
+		testParam.setRemark(rs.getString("remark"));
+		testParam.setDeviceType(rs.getString("device_type"));
+		return testParam;
+	}
+
+	/** 标漏点检记录分页查询 */
+	public static void getLeakCheckData() {
+		if (MesClient.leakRowData == null || MesClient.leakTable == null) {
+			return;
+		}
+		for (int i = 0; i < MesClient.leakRowData.length; i++) {
+			for (int j = 0; j < MesClient.leakRowData[i].length; j++) {
+				MesClient.leakRowData[i][j] = null;
+			}
+		}
+		int pageSize = 12;
+		int offset = (MesClient.leakPageNo - 1) * pageSize;
+		String keyword = MesClient.leakTextField != null ? MesClient.leakTextField.getText().trim() : "";
+
+		String sql = "SELECT * FROM leak_check_record";
+		String countSql = "select count(*) as total from leak_check_record";
+		if (keyword != null && !keyword.isEmpty()) {
+			sql += " WHERE ucode like '%" + keyword + "%' OR result like '%" + keyword + "%' OR program_no like '%" + keyword + "%'"
+					+ " OR create_time like '%" + keyword + "%' OR oprno like '%" + keyword + "%' OR oprno_title like '%" + keyword + "%'";
+			countSql += " WHERE ucode like '%" + keyword + "%' OR result like '%" + keyword + "%' OR program_no like '%" + keyword + "%'"
+					+ " OR create_time like '%" + keyword + "%' OR oprno like '%" + keyword + "%' OR oprno_title like '%" + keyword + "%'";
+		}
+		sql += " ORDER BY id DESC LIMIT " + pageSize + " OFFSET " + offset;
+
+		Statement stmt = null;
+		ResultSet ret = null;
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			stmt = conn.createStatement();
+			ResultSet countRet = stmt.executeQuery(countSql);
+			int totalItems = 0;
+			if (countRet.next()) {
+				totalItems = countRet.getInt("total");
+			}
+			countRet.close();
+			if (totalItems == 0) {
+				if (MesClient.leakPageLabel != null) {
+					MesClient.leakPageLabel.setText("0/0");
+				}
+				MesClient.leakTable.repaint();
+				return;
+			}
+			MesClient.leakTotalPages = (int) Math.ceil(totalItems * 1.0 / pageSize);
+			if (MesClient.leakPageLabel != null) {
+				MesClient.leakPageLabel.setText(MesClient.leakPageNo + "/" + MesClient.leakTotalPages);
+			}
+			ret = stmt.executeQuery(sql);
+			int rowIndex = 0;
+			while (ret.next() && rowIndex < MesClient.leakRowData.length) {
+				String pressure = getCompatCol(ret, "pressure", "param1");
+				String pressureUnit = getCompatCol(ret, "pressure_unit", "param2");
+				String leak = getCompatCol(ret, "leak", "param3");
+				String leakUnit = getCompatCol(ret, "leak_unit", "param4");
+				String oprno = ret.getString("oprno");
+				MesClient.leakRowData[rowIndex][0] = ret.getString("create_time");
+				MesClient.leakRowData[rowIndex][1] = oprno == null ? "" : oprno.trim();
+				MesClient.leakRowData[rowIndex][2] = ret.getString("ucode");
+				MesClient.leakRowData[rowIndex][3] = getCompatCol(ret, "program_no", "param5");
+				MesClient.leakRowData[rowIndex][4] = pressure + pressureUnit;
+				MesClient.leakRowData[rowIndex][5] = leak + leakUnit;
+				MesClient.leakRowData[rowIndex][6] = ret.getString("result");
+				try {
+					MesClient.leakRowData[rowIndex][7] = QmUploadUtil.formatUploadStatus(ret.getString("upload_status"));
+				} catch (SQLException ignored) {
+					MesClient.leakRowData[rowIndex][7] = "待上传";
+				}
+				rowIndex++;
+			}
+			MesClient.leakTable.repaint();
+			ret.close();
+		} catch (SQLException e1) {
+			e1.printStackTrace();
+		}
+	}
     
     //插入数据
     public static boolean insertData(String gw, String gy, String bw, String message_type, String sn) {
@@ -191,26 +640,137 @@ public class JdbcUtils {
 
 
 	public static boolean insertTestRecord(TestParam testParam){
-		boolean ret = false;
+		return insertTestRecordReturnId(testParam) > 0;
+	}
+
+	public static int insertTestRecordReturnId(TestParam testParam){
 		try {
-			// 确保连接已经打开
 			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
 				JdbcUtils.openConnection();
 			}
 			Statement statement=conn.createStatement();
-			String insertSQL = "INSERT INTO test_record (oprno,oprno_title,ucode, sn, line_sn, create_time, pressure, pressure_unit, leak, leak_unit, program_no, result, remark, device_type)" +
+			String insertSQL = "INSERT INTO test_record (oprno,oprno_title,ucode, sn, line_sn, create_time, pressure, pressure_unit, leak, leak_unit, program_no, result, remark, device_type,upload_status,upload_retry)" +
 					"VALUES('"+testParam.getOprno()+"','" + testParam.getOprnoTitle() + "','"+testParam.getUcode()+"', '" + testParam.getSn() + "', '" + testParam.getLineSn() + "', '" + testParam.getCreateTime() + "', '" + testParam.getPressure()+ "', '"
-					+testParam.getPressureUnit()+"','"+testParam.getLeak()+"','"+testParam.getLeakUnit()+"','"+testParam.getProgramNo()+"','"+testParam.getResult()+"','"+testParam.getRemark()+"','"+testParam.getDeviceType()+"')";
+					+testParam.getPressureUnit()+"','"+testParam.getLeak()+"','"+testParam.getLeakUnit()+"','"+testParam.getProgramNo()+"','"+testParam.getResult()+"','"+testParam.getRemark()+"','"+testParam.getDeviceType()+"','0',0)";
 			statement.executeUpdate(insertSQL);
+			ResultSet rs = statement.executeQuery("SELECT last_insert_rowid()");
+			int id = -1;
+			if (rs.next()) {
+				id = rs.getInt(1);
+			}
+			rs.close();
 			statement.close();
-			ret = true;
-			log.info("向test_record表插入数据成功: {}", insertSQL);
+			log.info("向test_record表插入数据成功, id={}", id);
+			return id;
 		} catch (SQLException e) {
-			ret = false;
 			log.error("向test_record表插入数据失败: {}", e.getMessage());
 			e.printStackTrace();
 		}
-		return ret;
+		return -1;
+	}
+
+	public static void updateUploadStatus(int id, String status) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("UPDATE test_record SET upload_status='" + status + "' WHERE id=" + id);
+			statement.close();
+		} catch (SQLException e) {
+			log.error("更新upload_status失败: {}", e.getMessage());
+		}
+	}
+
+	public static String getUploadStatus(int id) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			ResultSet rs = statement.executeQuery("SELECT upload_status FROM test_record WHERE id=" + id);
+			String status = "0";
+			if (rs.next()) {
+				status = rs.getString(1);
+			}
+			rs.close();
+			statement.close();
+			return status == null ? "0" : status;
+		} catch (SQLException e) {
+			log.error("读取upload_status失败 id={}", id, e);
+			return "0";
+		}
+	}
+
+	public static void incrementUploadRetry(int id) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("UPDATE test_record SET upload_retry=IFNULL(upload_retry,0)+1 WHERE id=" + id);
+			statement.close();
+		} catch (SQLException e) {
+			log.error("更新upload_retry失败: {}", e.getMessage());
+		}
+	}
+
+	public static int getUploadRetry(int id) {
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			ResultSet rs = statement.executeQuery("SELECT upload_retry FROM test_record WHERE id=" + id);
+			if (rs.next()) {
+				return rs.getInt(1);
+			}
+			rs.close();
+			statement.close();
+		} catch (SQLException e) {
+			log.error("查询upload_retry失败: {}", e.getMessage());
+		}
+		return 0;
+	}
+
+	public static List<TestParam> getPendingUploadRecords(int limit) {
+		List<TestParam> list = new ArrayList<>();
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			ResultSet rs = statement.executeQuery(
+					"SELECT * FROM test_record WHERE IFNULL(upload_status,'0') != '1' AND IFNULL(upload_retry,0) < 20 ORDER BY id ASC LIMIT " + limit);
+			while (rs.next()) {
+				list.add(mapTestParam(rs));
+			}
+			rs.close();
+			statement.close();
+		} catch (SQLException e) {
+			log.error("查询待上传记录失败: {}", e.getMessage());
+		}
+		return list;
+	}
+
+	private static TestParam mapTestParam(ResultSet rs) throws SQLException {
+		TestParam testParam = new TestParam();
+		testParam.setId(rs.getInt("id"));
+		testParam.setSn(rs.getString("sn"));
+		testParam.setOprno(rs.getString("oprno"));
+		testParam.setOprnoTitle(rs.getString("oprno_title"));
+		testParam.setUcode(rs.getString("ucode"));
+		testParam.setLineSn(rs.getString("line_sn"));
+		testParam.setCreateTime(rs.getString("create_time"));
+		testParam.setPressure(getCompatCol(rs, "pressure", "param1"));
+		testParam.setPressureUnit(getCompatCol(rs, "pressure_unit", "param2"));
+		testParam.setLeak(getCompatCol(rs, "leak", "param3"));
+		testParam.setLeakUnit(getCompatCol(rs, "leak_unit", "param4"));
+		testParam.setProgramNo(getCompatCol(rs, "program_no", "param5"));
+		testParam.setResult(rs.getString("result"));
+		testParam.setRemark(rs.getString("remark"));
+		testParam.setDeviceType(rs.getString("device_type"));
+		return testParam;
 	}
 
 	//面板查询(分页)
@@ -275,15 +835,18 @@ public class JdbcUtils {
 
 
 			while (ret.next()) {
-				MesClient.rowData[rowIndex][0] = ret.getString(2);
-				MesClient.rowData[rowIndex][1] = ret.getString(4);
-				MesClient.rowData[rowIndex][2] = ret.getString(5);
-				MesClient.rowData[rowIndex][3] = ret.getString(6)+ret.getString(7);
-				MesClient.rowData[rowIndex][4] = ret.getString(8)+ret.getString(9);
-				MesClient.rowData[rowIndex][5] = ret.getString(12);
-				MesClient.rowData[rowIndex][6] = ret.getString(13);
-//				MesClient.rowData[rowIndex][5] = ret.getString(8);
-//				MesClient.rowData[rowIndex][6] = ret.getString(9);
+				MesClient.rowData[rowIndex][0] = ret.getString("sn");
+				MesClient.rowData[rowIndex][1] = ret.getString("oprno");
+				MesClient.rowData[rowIndex][2] = ret.getString("oprno_title");
+				MesClient.rowData[rowIndex][3] = ret.getString("create_time");
+				MesClient.rowData[rowIndex][4] = getCompatCol(ret, "pressure", "param1") + getCompatCol(ret, "pressure_unit", "param2");
+				MesClient.rowData[rowIndex][5] = getCompatCol(ret, "leak", "param3") + getCompatCol(ret, "leak_unit", "param4");
+				MesClient.rowData[rowIndex][6] = ret.getString("result");
+				try {
+					MesClient.rowData[rowIndex][7] = QmUploadUtil.formatUploadStatus(ret.getString("upload_status"));
+				} catch (SQLException ignored) {
+					MesClient.rowData[rowIndex][7] = "待上传";
+				}
 				rowIndex++;
 			}
 
@@ -308,17 +871,17 @@ public class JdbcUtils {
 			log.info("selectSql:"+selectSql);
 			ResultSet rs = statement.executeQuery(selectSql);
 			while(rs.next()){
-				testParam.setSn(rs.getString(2));
-				testParam.setOprno(rs.getString(3));
-				testParam.setOprnoTitle(rs.getString(4));
-				testParam.setCreateTime(rs.getString(5));
-				testParam.setPressure(rs.getString(6));
-				testParam.setPressureUnit(rs.getString(7));
-				testParam.setLeak(rs.getString(8));
-				testParam.setLeakUnit(rs.getString(9));
-				testParam.setProgramNo(rs.getString(10));
-				testParam.setUcode(rs.getString(11));
-				testParam.setResult(rs.getString(12));
+				testParam.setSn(rs.getString("sn"));
+				testParam.setOprno(rs.getString("oprno"));
+				testParam.setOprnoTitle(rs.getString("oprno_title"));
+				testParam.setCreateTime(rs.getString("create_time"));
+				testParam.setPressure(getCompatCol(rs, "pressure", "param1"));
+				testParam.setPressureUnit(getCompatCol(rs, "pressure_unit", "param2"));
+				testParam.setLeak(getCompatCol(rs, "leak", "param3"));
+				testParam.setLeakUnit(getCompatCol(rs, "leak_unit", "param4"));
+				testParam.setProgramNo(getCompatCol(rs, "program_no", "param5"));
+				testParam.setUcode(rs.getString("ucode"));
+				testParam.setResult(rs.getString("result"));
 			}
 
 			statement.close();
@@ -415,12 +978,13 @@ public class JdbcUtils {
 
 	public static boolean insertTask(TaskParam taskParam){
 		boolean ret = false;
-		String record_time = DateLocalUtils.getCurrentTime();
 		try {
-			// 确保连接已经打开
 			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
 				JdbcUtils.openConnection();
 			}
+			if (!hasColumn("task", "cate")) {
+				tryAlter("ALTER TABLE task ADD COLUMN cate VARCHAR(1)");
+			}
 			Statement statement=conn.createStatement();
 
 			TaskConetnt taskConetnt = new TaskConetnt();
@@ -428,14 +992,21 @@ public class JdbcUtils {
 			taskConetnt.setTaskLeakage(taskParam.getTaskLeakage());
 			taskConetnt.setTaskLabelName(taskParam.getTaskLabelName());
 			String content = JSONObject.toJSONString(taskConetnt);
+			String cate = taskParam.getCate() == null ? "0" : taskParam.getCate();
+			String title = taskParam.getTitle() == null ? "" : taskParam.getTitle().replace("'", "''");
+			String barcodeSpec = taskParam.getBarcodeSpec() == null ? "" : taskParam.getBarcodeSpec().replace("'", "''");
+			String barcodeSpecStart = taskParam.getBarcodeSpecStart() == null ? "" : taskParam.getBarcodeSpecStart();
+			String barcodeLength = taskParam.getBarcodeLength() == null ? "" : taskParam.getBarcodeLength();
 
 			String insertSQL = "INSERT INTO task (title,cate,barcode_spec,barcode_spec_start,barcode_length,content)"
-					+ "VALUES('"+taskParam.getTitle()+"','"+taskParam.getCate()+"','"+taskParam.getBarcodeSpec()+"','"+taskParam.getBarcodeSpecStart()+"','"+taskParam.getBarcodeLength()+"','" + content +"')";
+					+ "VALUES('"+title+"','"+cate+"','"+barcodeSpec+"','"+barcodeSpecStart+"','"+barcodeLength+"','" + content +"')";
 			statement.executeUpdate(insertSQL);
 			statement.close();
 			ret = true;
+			log.info("新增任务成功: title={}, cate={}", title, cate);
 		} catch (SQLException e) {
 			ret = false;
+			log.error("新增任务失败: {}", e.getMessage(), e);
 		}
 		return ret;
 	}
@@ -516,7 +1087,12 @@ public class JdbcUtils {
 				taskParam.setBarcodeSpec(rs.getString("barcode_spec"));
 				taskParam.setBarcodeSpecStart(rs.getString("barcode_spec_start"));
 				taskParam.setBarcodeLength(rs.getString("barcode_length"));
-				taskParam.setCate(rs.getString("cate"));
+				try {
+					String cate = rs.getString("cate");
+					taskParam.setCate(cate == null || cate.isEmpty() ? "0" : cate);
+				} catch (SQLException ignored) {
+					taskParam.setCate("0");
+				}
 
 				String content = rs.getString("content");
 				if(content != null && !content.isEmpty()){

+ 130 - 0
src/com/mes/util/LeakCheckUploadUtil.java

@@ -0,0 +1,130 @@
+package com.mes.util;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.ui.DataUtil;
+import com.mes.ui.MesClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.SwingUtilities;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 标漏点检本地保存 + 异步上传 MES
+ */
+public class LeakCheckUploadUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(LeakCheckUploadUtil.class);
+    private static Timer retryTimer;
+    private static final long RETRY_INTERVAL_MS = 30000L;
+    private static final int MAX_RETRY = 20;
+
+    private static final ExecutorService uploadExecutor = Executors.newSingleThreadExecutor(r -> {
+        Thread t = new Thread(r, "LeakCheckUploadWorker");
+        t.setDaemon(true);
+        return t;
+    });
+
+    public static boolean saveAndUpload(TestParam testParam) {
+        int recordId = JdbcUtils.insertLeakCheckRecordReturnId(testParam);
+        if (recordId <= 0) {
+            runOnEdt(() -> MesClient.setMenuStatus("标漏记录保存失败", -1));
+            return false;
+        }
+        testParam.setId(recordId);
+
+        if (!MesClient.mes_enable) {
+            JdbcUtils.updateLeakCheckUploadStatus(recordId, "1");
+            refreshLeakRecordQuery();
+            return true;
+        }
+        refreshLeakRecordQuery();
+        submitAsyncUpload(testParam);
+        return true;
+    }
+
+    private static void refreshLeakRecordQuery() {
+        runOnEdt(() -> {
+            try {
+                JdbcUtils.getLeakCheckData();
+            } catch (Exception ignored) {
+            }
+        });
+    }
+
+    private static void submitAsyncUpload(TestParam testParam) {
+        if (testParam == null || testParam.getId() == null || testParam.getId() <= 0) {
+            return;
+        }
+        uploadExecutor.execute(() -> doUpload(testParam));
+    }
+
+    private static void doUpload(TestParam testParam) {
+        if (!MesClient.mes_enable) {
+            JdbcUtils.updateLeakCheckUploadStatus(testParam.getId(), "1");
+            return;
+        }
+        try {
+            JSONObject retObj = DataUtil.qmLeakCheckResultData(testParam);
+            if (QmUploadUtil.isUploadSuccess(retObj)) {
+                JdbcUtils.updateLeakCheckUploadStatus(testParam.getId(), "1");
+                log.info("标漏结果上传成功, recordId={}", testParam.getId());
+                refreshLeakRecordQuery();
+                return;
+            }
+            markFailed(testParam.getId());
+            log.warn("标漏结果上传失败, recordId={}", testParam.getId());
+        } catch (Exception e) {
+            log.error("标漏结果上传异常, recordId=" + testParam.getId(), e);
+            markFailed(testParam.getId());
+        }
+    }
+
+    private static void markFailed(int recordId) {
+        JdbcUtils.updateLeakCheckUploadStatus(recordId, "2");
+        JdbcUtils.incrementLeakCheckUploadRetry(recordId);
+        refreshLeakRecordQuery();
+    }
+
+    public static void startRetryTimer() {
+        if (retryTimer != null) {
+            retryTimer.cancel();
+        }
+        retryTimer = new Timer("LeakCheckUploadRetry", true);
+        retryTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                retryPendingUploads();
+            }
+        }, RETRY_INTERVAL_MS, RETRY_INTERVAL_MS);
+    }
+
+    public static void retryPendingUploads() {
+        if (!MesClient.mes_enable) {
+            return;
+        }
+        List<TestParam> pending = JdbcUtils.getPendingLeakCheckUploadRecords(10);
+        if (pending == null || pending.isEmpty()) {
+            return;
+        }
+        for (TestParam testParam : pending) {
+            int retry = JdbcUtils.getLeakCheckUploadRetry(testParam.getId());
+            if (retry >= MAX_RETRY) {
+                continue;
+            }
+            submitAsyncUpload(testParam);
+        }
+    }
+
+    private static void runOnEdt(Runnable action) {
+        if (SwingUtilities.isEventDispatchThread()) {
+            action.run();
+        } else {
+            SwingUtilities.invokeLater(action);
+        }
+    }
+}

+ 207 - 0
src/com/mes/util/LeakCheckUtil.java

@@ -0,0 +1,207 @@
+package com.mes.util;
+
+import com.mes.ui.ConfigParam;
+import com.mes.ui.MesClient;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 标漏点检:班次日判定、阈值区间、程序号等。
+ * 班次日 = [重置时刻, 次日重置时刻),默认 07:30。
+ * 左右工位各自独立:左侧标漏只影响左气密,右侧只影响右气密。
+ */
+public final class LeakCheckUtil {
+
+    public static final String APP_KEY_OK_TIME_LEFT = "leak_check_ok_time_L";
+    public static final String APP_KEY_OK_TIME_RIGHT = "leak_check_ok_time_R";
+    /** 兼容旧版单键,迁移后不再写入 */
+    public static final String APP_KEY_OK_TIME_LEGACY = "leak_check_ok_time";
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private LeakCheckUtil() {
+    }
+
+    public static boolean isEnabled() {
+        ConfigParam cfg = MesClient.configParam;
+        if (cfg == null) {
+            return true;
+        }
+        String v = cfg.getLeakCheckEnable();
+        return v == null || v.isEmpty() || "是".equals(v);
+    }
+
+    public static String leakProgramNo() {
+        ConfigParam cfg = MesClient.configParam;
+        if (cfg == null || cfg.getLeakProgramNo() == null || cfg.getLeakProgramNo().trim().isEmpty()) {
+            return "10";
+        }
+        return cfg.getLeakProgramNo().trim();
+    }
+
+    public static String qmProgramNo() {
+        return qmProgramNoForSide(0);
+    }
+
+    /** side==1 右工位用 programNo2,否则左工位 programNo */
+    public static String qmProgramNoForSide(int side) {
+        ConfigParam cfg = MesClient.configParam;
+        if (cfg == null) {
+            return "1";
+        }
+        if (side == 1) {
+            if (cfg.getProgramNo2() == null || cfg.getProgramNo2().trim().isEmpty()) {
+                return "1";
+            }
+            return cfg.getProgramNo2().trim();
+        }
+        if (cfg.getProgramNo() == null || cfg.getProgramNo().trim().isEmpty()) {
+            return "1";
+        }
+        return cfg.getProgramNo().trim();
+    }
+
+    /** 从字符串中提取第一个数值(去掉单位、空格等) */
+    public static Double parseNumber(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        String s = raw.trim().replace(',', '.');
+        if (s.isEmpty()) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        boolean seenDot = false;
+        for (int i = 0; i < s.length(); i++) {
+            char c = s.charAt(i);
+            if (c == '-' && sb.length() == 0) {
+                sb.append(c);
+            } else if (c >= '0' && c <= '9') {
+                sb.append(c);
+            } else if ((c == '.' || c == '.') && !seenDot) {
+                sb.append('.');
+                seenDot = true;
+            } else if (sb.length() > 0) {
+                break;
+            }
+        }
+        if (sb.length() == 0 || "-".equals(sb.toString()) || ".".equals(sb.toString())) {
+            return null;
+        }
+        try {
+            return Double.parseDouble(sb.toString());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    public static double leakMin() {
+        Double v = parseNumber(MesClient.configParam != null ? MesClient.configParam.getLeakMin() : null);
+        return v != null ? v : 7.8;
+    }
+
+    public static double leakMax() {
+        Double v = parseNumber(MesClient.configParam != null ? MesClient.configParam.getLeakMax() : null);
+        return v != null ? v : 8.0;
+    }
+
+    public static LocalTime resetTime() {
+        try {
+            String v = MesClient.configParam != null ? MesClient.configParam.getLeakResetTime() : null;
+            if (v == null || v.trim().isEmpty()) {
+                return LocalTime.of(7, 30);
+            }
+            String[] parts = v.trim().split(":");
+            int h = Integer.parseInt(parts[0]);
+            int m = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
+            return LocalTime.of(h, m);
+        } catch (Exception e) {
+            return LocalTime.of(7, 30);
+        }
+    }
+
+    /** 当前班次起始时刻(含) */
+    public static LocalDateTime currentShiftStart() {
+        LocalDateTime now = LocalDateTime.now();
+        LocalTime rt = resetTime();
+        LocalDateTime todayStart = LocalDateTime.of(now.toLocalDate(), rt);
+        if (now.isBefore(todayStart)) {
+            return todayStart.minusDays(1);
+        }
+        return todayStart;
+    }
+
+    private static String okTimeKey(int side) {
+        return side == 1 ? APP_KEY_OK_TIME_RIGHT : APP_KEY_OK_TIME_LEFT;
+    }
+
+    private static boolean isTimeInCurrentShift(String okTime) {
+        if (okTime == null || okTime.trim().isEmpty()) {
+            return false;
+        }
+        try {
+            LocalDateTime ok = LocalDateTime.parse(okTime.trim(), TIME_FMT);
+            return !ok.isBefore(currentShiftStart());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /** 指定工位本班是否已标漏通过;side=0左 / 1右 */
+    public static boolean isPassedForCurrentShift(int side) {
+        if (!isEnabled()) {
+            return true;
+        }
+        if (isTimeInCurrentShift(JdbcUtils.getAppConfig(okTimeKey(side), ""))) {
+            return true;
+        }
+        // 兼容旧版:仅有总键且有效时,视为两侧都已通过(仅读,不自动拆分写入)
+        return isTimeInCurrentShift(JdbcUtils.getAppConfig(APP_KEY_OK_TIME_LEGACY, ""));
+    }
+
+    /** 左右本班是否都已通过 */
+    public static boolean isBothPassedForCurrentShift() {
+        if (!isEnabled()) {
+            return true;
+        }
+        return isPassedForCurrentShift(0) && isPassedForCurrentShift(1);
+    }
+
+    /** @deprecated 使用 {@link #isBothPassedForCurrentShift()} */
+    public static boolean isPassedForCurrentShift() {
+        return isBothPassedForCurrentShift();
+    }
+
+    public static void markPassedNow(int side) {
+        JdbcUtils.setAppConfig(okTimeKey(side), LocalDateTime.now().format(TIME_FMT));
+    }
+
+    /** @deprecated 使用 {@link #markPassedNow(int)} */
+    public static void markPassedNow() {
+        markPassedNow(0);
+        markPassedNow(1);
+    }
+
+    public static void clearPassed() {
+        JdbcUtils.setAppConfig(APP_KEY_OK_TIME_LEFT, "");
+        JdbcUtils.setAppConfig(APP_KEY_OK_TIME_RIGHT, "");
+        JdbcUtils.setAppConfig(APP_KEY_OK_TIME_LEGACY, "");
+    }
+
+    public static void clearPassed(int side) {
+        JdbcUtils.setAppConfig(okTimeKey(side), "");
+    }
+
+    /** 泄漏率是否在 [min, max] 闭区间(保留3位小数比较,避免浮点误差) */
+    public static boolean isLeakValueOk(double leakValue) {
+        double v = Math.round(leakValue * 1000.0) / 1000.0;
+        double min = leakMin();
+        double max = leakMax();
+        return v >= min && v <= max;
+    }
+
+    public static String sideLabel(int side) {
+        return side == 1 ? "右" : "左";
+    }
+}

+ 180 - 0
src/com/mes/util/QmUploadUtil.java

@@ -0,0 +1,180 @@
+package com.mes.util;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.ui.DataUtil;
+import com.mes.ui.MesClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.SwingUtilities;
+import java.util.List;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 气密结果本地保存 + 后台异步上传 MES + 失败自动重传(不阻塞主程序)
+ */
+public class QmUploadUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(QmUploadUtil.class);
+    private static Timer retryTimer;
+    private static final long RETRY_INTERVAL_MS = 30000L;
+    private static final int MAX_RETRY = 20;
+
+    private static final ExecutorService uploadExecutor = Executors.newSingleThreadExecutor(r -> {
+        Thread t = new Thread(r, "QmUploadWorker");
+        t.setDaemon(true);
+        return t;
+    });
+
+    public static boolean isUploadSuccess(JSONObject retObj) {
+        return retObj != null && retObj.get("result") != null
+                && retObj.get("result").toString().equalsIgnoreCase("true");
+    }
+
+    public static String formatUploadStatus(String status) {
+        if ("1".equals(status)) {
+            return "已上传";
+        }
+        if ("2".equals(status)) {
+            return "待重传";
+        }
+        return "待上传";
+    }
+
+    /**
+     * 保存本地记录并提交后台上传。返回 true 表示本地保存成功(主流程可继续,不等待 MES 上传)。
+     */
+    public static boolean saveAndUpload(TestParam testParam, boolean resetOnSuccess) {
+        int recordId = JdbcUtils.insertTestRecordReturnId(testParam);
+        if (recordId <= 0) {
+            runOnEdt(() -> MesClient.setMenuStatus("本地记录保存失败", -1));
+            return false;
+        }
+        testParam.setId(recordId);
+        refreshRecordQuery();
+
+        if (!MesClient.mes_enable) {
+            JdbcUtils.updateUploadStatus(recordId, "1");
+            if (MesClient.isSealRingEnabled()) {
+                int side = MesClient.resolveSealRingSide(testParam);
+                runOnEdt(() -> MesClient.onSealRingUploadSuccess(side));
+            }
+            if (resetOnSuccess) {
+                runOnEdt(() -> {
+                    MesClient.resetScanA();
+                    MesClient.setMenuStatus("测试结果已保存,请扫下一件", 0);
+                });
+            }
+            return true;
+        }
+
+        if (resetOnSuccess) {
+            runOnEdt(() -> {
+                MesClient.resetScanA();
+                MesClient.setMenuStatus("测试结果已保存,后台上传中...", 0);
+            });
+        }
+
+        submitAsyncUpload(testParam, resetOnSuccess);
+        return true;
+    }
+
+    public static void uploadRecord(TestParam testParam, boolean resetOnSuccess) {
+        submitAsyncUpload(testParam, resetOnSuccess);
+    }
+
+    private static void submitAsyncUpload(TestParam testParam, boolean resetOnSuccess) {
+        if (testParam == null || testParam.getId() == null || testParam.getId() <= 0) {
+            return;
+        }
+        uploadExecutor.execute(() -> doUpload(testParam, resetOnSuccess));
+    }
+
+    private static void doUpload(TestParam testParam, boolean resetOnSuccess) {
+        if (!MesClient.mes_enable) {
+            JdbcUtils.updateUploadStatus(testParam.getId(), "1");
+            return;
+        }
+        try {
+            JSONObject retObj = DataUtil.qmResultData(testParam);
+            if (isUploadSuccess(retObj)) {
+                String oldStatus = JdbcUtils.getUploadStatus(testParam.getId());
+                JdbcUtils.updateUploadStatus(testParam.getId(), "1");
+                if (!"1".equals(oldStatus) && MesClient.isSealRingEnabled()) {
+                    int side = MesClient.resolveSealRingSide(testParam);
+                    runOnEdt(() -> MesClient.onSealRingUploadSuccess(side));
+                }
+                runOnEdt(() -> {
+                    if (resetOnSuccess) {
+                        MesClient.setMenuStatus("测试结果上传成功", 0);
+                    }
+                });
+                refreshRecordQuery();
+                log.info("气密结果上传成功, recordId={}", testParam.getId());
+                return;
+            }
+            markUploadFailed(testParam.getId());
+            log.warn("气密结果上传失败, recordId={}, 将后台重试", testParam.getId());
+        } catch (Exception e) {
+            log.error("气密结果上传异常, recordId=" + testParam.getId(), e);
+            markUploadFailed(testParam.getId());
+        }
+    }
+
+    private static void markUploadFailed(int recordId) {
+        JdbcUtils.updateUploadStatus(recordId, "2");
+        JdbcUtils.incrementUploadRetry(recordId);
+        runOnEdt(() -> MesClient.setMenuStatus("测试结果上传失败,后台自动重试中", -1));
+        refreshRecordQuery();
+    }
+
+    public static void startRetryTimer() {
+        if (retryTimer != null) {
+            retryTimer.cancel();
+        }
+        retryTimer = new Timer("QmUploadRetry", true);
+        retryTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                retryPendingUploads();
+            }
+        }, RETRY_INTERVAL_MS, RETRY_INTERVAL_MS);
+    }
+
+    public static void retryPendingUploads() {
+        if (!MesClient.mes_enable) {
+            return;
+        }
+        List<TestParam> pending = JdbcUtils.getPendingUploadRecords(10);
+        if (pending == null || pending.isEmpty()) {
+            return;
+        }
+        for (TestParam testParam : pending) {
+            int retry = JdbcUtils.getUploadRetry(testParam.getId());
+            if (retry >= MAX_RETRY) {
+                continue;
+            }
+            submitAsyncUpload(testParam, false);
+        }
+    }
+
+    private static void refreshRecordQuery() {
+        runOnEdt(() -> {
+            try {
+                JdbcUtils.getTestData();
+            } catch (Exception ignored) {
+            }
+        });
+    }
+
+    private static void runOnEdt(Runnable action) {
+        if (SwingUtilities.isEventDispatchThread()) {
+            action.run();
+        } else {
+            SwingUtilities.invokeLater(action);
+        }
+    }
+}

+ 131 - 106
src/com/mes/util/SerialPortUtils.java

@@ -31,7 +31,7 @@ public class SerialPortUtils {
             this.portName = portName;
             this.oprnoName = oprnoName;
             this.oprno = oprno;
-            this.device = device;
+            this.device = device == null ? "" : device.trim().toLowerCase();
             serialPort = SerialPort.getCommPort(portName);
             serialPort.setBaudRate(Integer.valueOf(MesClient.configParam.getBaudRate()));
             serialPort.setNumDataBits(Integer.valueOf(MesClient.configParam.getDataBits()));
@@ -101,124 +101,115 @@ public class SerialPortUtils {
 //                                        String readProgramNo = "PRG_"+String.format("%03d", Integer.parseInt(MesClient.configParam.getProgramNo()));
 //                                        sendData(readProgramNo);
 
+                                            // 对齐 qm:RUN/过程帧 → tjFlag=2;过程刷新实时值;结束帧出结果
                                             if (MesClient.tjFlag == 1) {
-                                                if (data.trim().equals("RUN") || data.split("\t").length == 3) {
-                                                    log.info("运行开始了");
+                                                if (data.trim().equals("RUN") || InnomatecEndFrame.isProgressLine(data)) {
+                                                    log.info("左工位运行开始了");
                                                     MesClient.tjFlag = 2;
-//                                                    String readProgramNo = "READ_"+String.format("%03d", Integer.parseInt(MesClient.programNo.getText().trim()));
-//                                                    sendData(readProgramNo);
+                                                    MesClient.setMenuStatus("左工位设备运行中", 0);
                                                 }
                                             }
-                                            if (MesClient.tjFlag == 2) {
-                                                if (data.split("\t").length == 3) {//例  data= 2.5	1.1	0.000   时间(s) 压力(hpa)  压降(pa)
-                                                    String[] lists = data.split("\\s+"); // 使用空格进行分割,\\s+ 表示匹配一个或多个空白字符
+                                            if (MesClient.tjFlag == 2 || MesClient.tjFlag == 1) {
+                                                if (InnomatecEndFrame.isProgressLine(data)) {
+                                                    String[] lists = InnomatecEndFrame.progressValues(data);
                                                     if (lists.length == 3) {
-                                                        double yali = Double.parseDouble(lists[1].trim());
-                                                        double dividedValue = yali / 10;
-                                                        // Format to 2 decimal places
-                                                        DecimalFormat df = new DecimalFormat("#.##");
-                                                        String result = df.format(dividedValue);
-                                                        MesClient.param1.setText(result);
-                                                        MesClient.param2.setText(lists[2]);
-//
+                                                        try {
+                                                            double yali = Double.parseDouble(lists[1].trim());
+                                                            String pressure = new DecimalFormat("#.##").format(yali / 10);
+                                                            String leak = lists[2];
+                                                            javax.swing.SwingUtilities.invokeLater(() -> {
+                                                                MesClient.param1.setText(pressure);
+                                                                MesClient.param2.setText(leak);
+                                                                MesClient.param1.repaint();
+                                                                MesClient.param2.repaint();
+                                                            });
+                                                        } catch (Exception pe) {
+                                                            log.warn("左工位实时过程解析失败: {}", pe.getMessage());
+                                                        }
                                                     }
-                                                } else if (data.length() == 97 || data.length() == 98) {//检测结束
-                                                    log.info("检测结束");
-                                                    //结果
-                                                    String result = null;
-                                                    String yaLi = null;
-                                                    String yaJiang = null;
-                                                    String leakRate = null;
-                                                    String leakRateUnit = null;
+                                                } else if (InnomatecEndFrame.looksLikeEndFrame(data)) {
+                                                    log.info("左工位检测结束 rawLen={} data=[{}]", data.length(), data.replace("\r", "\\r").replace("\n", "\\n"));
                                                     try {
-                                                        result = data.substring(86, 98).trim();          // "PNE"(包含末尾的 E)
-                                                        yaLi = data.substring(35, 43).trim();            // "+ 224.58"(不变,已完整)
-                                                        String yaLiUnit = data.substring(45, 49).trim(); // "kPa"(不变)
-                                                        yaJiang = data.substring(51, 60).trim();         // "+ 0.00"(原缺最后一个 0,end+1)
-                                                        String yaJiangUnit = data.substring(61, 65).trim(); // "Pa"(不变)
-                                                        leakRate = data.substring(67, 76).trim();        // "+ 0.00"(原缺最后一个 0,end+1)
-                                                        leakRateUnit = data.substring(77, 85).trim();    // "ml/min"(原缺 n,end+1)
+                                                        InnomatecEndFrame end = InnomatecEndFrame.parse(data);
+                                                        log.info("左工位结束帧 frameLen={} result={} yaLi={} leak={}",
+                                                                end.frameLen, end.result, end.yaLi, end.leakRate);
+                                                        final String result = end.result;
+                                                        final String yaLi = end.yaLi;
+                                                        final String yaLiUnit = end.yaLiUnit;
+                                                        final String yaJiang = end.yaJiang;
+                                                        final String yaJiangUnit = end.yaJiangUnit;
+                                                        final String leakRate = end.leakRate;
+                                                        final String leakRateUnit = end.leakRateUnit;
+
+                                                        // 现场统一按泄漏率 ml/min(勿用压降 Pa)
+                                                        final String leakValue = leakRate;
+                                                        final String leakValueUnit = "ml/min";
+                                                        final String lastRet = InnomatecEndFrame.isPassResult(result) ? "OK" : "NG";
+                                                        final String sn = MesClient.product_sn1.getText().trim();
+                                                        final String deviceType = device;
 
                                                         MesClient.tjFlag = 3;
-//                                                    MesClient.setMenuStatus(MesClient.tjFlagText3,0);
-
-                                                        String leakValue = "";
-                                                        String leakValueUnit = "";
-                                                        if (MesClient.configParam.getLeakage().equals("压降")) {
-                                                            leakValue = yaJiang;
-                                                            leakValueUnit = yaJiangUnit;
-                                                        } else {
-                                                            leakValue = leakRate;
-                                                            leakValueUnit = leakRateUnit;
-                                                        }
-
-                                                        MesClient.param1.setText(yaLi + yaLiUnit);
-                                                        MesClient.param2.setText(leakValue + leakValueUnit);
-                                                        String lastRet = "NG";
-                                                        if (result.equals("OK")) {
-                                                            MesClient.result1.setText("OK");
-                                                            MesClient.result1.setForeground(Color.GREEN);
-                                                            lastRet = "OK";
-                                                        } else {
-                                                            MesClient.result1.setText("NG");
-                                                            MesClient.result1.setForeground(Color.RED);
-                                                        }
-
-                                                        TestParam testParam = new TestParam();
-                                                        testParam.setSn(MesClient.product_sn1.getText().trim());
-                                                        testParam.setPressure(yaLi);
-                                                        testParam.setPressureUnit(yaLiUnit);
-                                                        testParam.setLeak(leakValue);
-                                                        testParam.setLeakUnit(leakValueUnit);
-                                                        testParam.setOprno(MesClient.configParam.getOprno());
-                                                        testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle());
-                                                        testParam.setProgramNo(MesClient.configParam.getProgramNo());
-                                                        testParam.setUcode(MesClient.user_menu.getText().trim());
-                                                        testParam.setResult(lastRet);
-                                                        if (lastRet.equals("NG")) {
-                                                            testParam.setRemark(result);
-                                                        }else {
-                                                            testParam.setRemark("");
-                                                        }
-
-                                                        String testDate = DateLocalUtils.getCurrentTime();
-                                                        testParam.setCreateTime(testDate);
-                                                        JdbcUtils.insertTestRecord(testParam);
-
-//                                                    if(MesClient.configParam.getPrintLabel().equals("是") && lastRet.equals("OK")){
-//                                                        PrintUtil.printLabel(testParam.getSn(),testParam.getPressure()+testParam.getPressureUnit(),testParam.getLeak()+testParam.getLeakUnit(),testDate);
-//                                                    }
-
-//                                                        JSONObject retObj = DataUtil.qmresult("500001", testParam);
-                                                        JSONObject retObj = DataUtil.qmResultData(testParam);
-                                                        if (retObj == null || retObj.get("result") == null) {
-                                                            MesClient.setMenuStatus("测试结果上传失败,请重试", -1);
-                                                            return;
-                                                        }
-                                                        if (retObj.get("result").toString().equalsIgnoreCase("true")) {
-                                                            MesClient.work_status = 0;
-                                                            MesClient.setMenuStatus("测试结果上传成功,请扫下一件", 0);
-                                                        } else {
-                                                            MesClient.setMenuStatus("测试结果上传失败,请重试", -1);
-                                                            return;
-                                                        }
-
+                                                        MesClient.work_status = 0;
+                                                        MesClient.mesStartFlag = 0;
+
+                                                        javax.swing.SwingUtilities.invokeLater(() -> {
+                                                            MesClient.setMenuStatus("左工位设备运行结束", 0);
+                                                            MesClient.param1.setText(yaLi + yaLiUnit);
+                                                            MesClient.param2.setText(leakValue + leakValueUnit);
+                                                            if ("OK".equals(lastRet)) {
+                                                                MesClient.result1.setText("OK");
+                                                                MesClient.result1.setForeground(Color.GREEN);
+                                                            } else {
+                                                                MesClient.result1.setText("NG");
+                                                                MesClient.result1.setForeground(Color.RED);
+                                                            }
+                                                            MesClient.result1.repaint();
+                                                            MesClient.param1.repaint();
+                                                            MesClient.param2.repaint();
+
+                                                            if (MesClient.leakCheckRunningL) {
+                                                                MesClient.finishLeakCheck(0, yaLi, yaLiUnit, leakRate, leakRateUnit, deviceType);
+                                                                return;
+                                                            }
+
+                                                            TestParam testParam = new TestParam();
+                                                            testParam.setSn(sn);
+                                                            testParam.setPressure(yaLi);
+                                                            testParam.setPressureUnit(yaLiUnit);
+                                                            testParam.setLeak(leakValue);
+                                                            testParam.setLeakUnit(leakValueUnit);
+                                                            testParam.setOprno(MesClient.configParam.getOprno());
+                                                            testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle());
+                                                            testParam.setProgramNo(MesClient.configParam.getProgramNo());
+                                                            testParam.setUcode(MesClient.user_menu.getText().trim());
+                                                            testParam.setResult(lastRet);
+                                                            testParam.setRemark("OK".equals(lastRet) ? "" : result);
+                                                            String testDate = DateLocalUtils.getCurrentTime();
+                                                            testParam.setCreateTime(testDate);
+                                                            testParam.setLineSn(MesClient.configParam.getLineSn());
+                                                            testParam.setDeviceType(deviceType);
+                                                            if (QmUploadUtil.saveAndUpload(testParam, false)) {
+                                                                MesClient.resetScanA();
+                                                                MesClient.setMenuStatus("左工位测试结果已保存,请扫下一件", 0);
+                                                            }
+                                                            if (MesClient.configParam.getPrintLabel() != null
+                                                                    && MesClient.configParam.getPrintLabel().equals("是")
+                                                                    && "OK".equals(lastRet)) {
+                                                                try {
+                                                                    PrintUtil.printLabel(testParam.getSn(),
+                                                                            testParam.getPressure() + testParam.getPressureUnit(),
+                                                                            testParam.getLeak() + testParam.getLeakUnit(),
+                                                                            testDate);
+                                                                } catch (Exception pe) {
+                                                                    log.error("打印报错", pe);
+                                                                }
+                                                            }
+                                                        });
                                                     } catch (Exception e) {
-                                                        log.info(e.getMessage());
+                                                        log.error("左工位结束帧解析失败 rawLen=" + data.length(), e);
                                                     }
-
-                                                } else if (data.length() == 165 || data.contains("PARA_")) {//
-//                                                    String cq = data.substring(8, 13).trim();//充气
-//                                                    String by = data.substring(13, 18).trim();//保压
-//                                                    String cs = data.substring(18, 23).trim();//测试
-//                                                    MesClient.bao_ya_text.setText(by + "(s)");
-//                                                    MesClient.chong_qi_text.setText(cq + "(s)");
-//                                                    MesClient.ce_shi_text.setText(cs + "(s)");
                                                 }
                                             }
-                                            if (MesClient.tjFlag == 3) {
-//
-                                            }
                                         }
                                     } else {
 //                                    sendData("STOP");
@@ -345,4 +336,38 @@ public class SerialPortUtils {
 
     }
 
+    public boolean changeProgram() {
+        return changeProgram(MesClient.configParam != null ? MesClient.configParam.getProgramNo() : "1");
+    }
+
+    public boolean changeProgram(String programNoStr) {
+        if (isOpen) {
+            int programNoVal;
+            try {
+                programNoVal = Integer.parseInt(programNoStr == null || programNoStr.trim().isEmpty() ? "1" : programNoStr.trim());
+            } catch (Exception e) {
+                programNoVal = 1;
+            }
+            if (device.equals("ateq")) {
+                String s1 = DataUtils.CRC16(ATEQ.getStationNo() + " 10 02 00 00 01 02 " + DataUtils.intToWord(programNoVal - 1));
+                log.info("切换程序{} -> {}", programNoVal, s1);
+                byte[] read = this.read(s1);
+                String s2 = DataUtils.CRC16(ATEQ.getStationNo() + " 10 30 04 00 01");
+                boolean result = Arrays.equals(read, DataUtils.hexStringToByteArray(s2));
+                log.info("切换程序{}", result ? "成功" : "失败");
+                return result;
+            } else if (device.equals("innomatec")) {
+                String readProgramNo = "PRG_" + String.format("%03d", programNoVal);
+                this.sendData(readProgramNo);
+                return true;
+            }
+            return true;
+        } else {
+            log.info("成品气密仪串口未打开,尝试重新打开串口...");
+            MesClient.setMenuStatus("成品气密仪连接失败,请检查设备后重试", 1);
+            isOpen = openPort();
+            return false;
+        }
+    }
+
 }

+ 133 - 119
src/com/mes/util/SerialPortUtils2.java

@@ -6,7 +6,6 @@ import com.fazecast.jSerialComm.SerialPortDataListener;
 import com.fazecast.jSerialComm.SerialPortEvent;
 import com.mes.ui.DataUtil;
 import com.mes.ui.MesClient;
-import jdk.nashorn.internal.runtime.regexp.joni.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,7 +31,7 @@ public class SerialPortUtils2 {
             this.oprnoName = oprnoName;
             this.oprno = oprno;
 //            this.device = "innomatec";
-            this.device = device;
+            this.device = device == null ? "" : device.trim().toLowerCase();
             serialPort = SerialPort.getCommPort(portName);
             serialPort.setBaudRate(Integer.valueOf(MesClient.configParam.getBaudRate2()));
             serialPort.setNumDataBits(Integer.valueOf(MesClient.configParam.getDataBits2()));
@@ -103,137 +102,118 @@ public class SerialPortUtils2 {
 //                                        String readProgramNo = "PRG_"+String.format("%03d", Integer.parseInt(MesClient.configParam.getProgramNo2()));
 //                                        sendData(readProgramNo);
 
-                                            if (MesClient.tjFlag == 1) {
-                                                if (data.trim().equals("RUN") || data.split("\t").length == 3) {
-                                                    log.info("运行开始了");
-                                                    MesClient.tjFlag = 2;
+                                            // 对齐 qm / 左工位:过程帧刷新实时值;结束帧出结果
+                                            if (MesClient.tjFlag2 == 1) {
+                                                if (data.trim().equals("RUN") || InnomatecEndFrame.isProgressLine(data)) {
+                                                    log.info("右工位运行开始了");
+                                                    MesClient.tjFlag2 = 2;
+                                                    MesClient.setMenuStatus("右工位设备运行中", 0);
                                                 }
                                             }
-                                            if (MesClient.tjFlag2 == 2) {
-                                                if (data.split("\t").length == 3) {//例  data= 2.5	1.1	0.000   时间(s) 压力(hpa)  压降(pa)
-                                                    String[] lists = data.split("\\s+"); // 使用空格进行分割,\\s+ 表示匹配一个或多个空白字符
+                                            if (MesClient.tjFlag2 == 2 || MesClient.tjFlag2 == 1) {
+                                                if (InnomatecEndFrame.isProgressLine(data)) {
+                                                    String[] lists = InnomatecEndFrame.progressValues(data);
                                                     if (lists.length == 3) {
-                                                        double yali = Double.parseDouble(lists[1].trim());
-                                                        double dividedValue = yali / 10;
-                                                        // Format to 2 decimal places
-                                                        DecimalFormat df = new DecimalFormat("#.##");
-                                                        String result = df.format(dividedValue);
-                                                        MesClient.param3.setText(result);
-                                                        MesClient.param4.setText(lists[2]);
-//
+                                                        try {
+                                                            double yali = Double.parseDouble(lists[1].trim());
+                                                            String pressure = new DecimalFormat("#.##").format(yali / 10);
+                                                            String leak = lists[2];
+                                                            javax.swing.SwingUtilities.invokeLater(() -> {
+                                                                MesClient.param3.setText(pressure);
+                                                                MesClient.param4.setText(leak);
+                                                                MesClient.param3.repaint();
+                                                                MesClient.param4.repaint();
+                                                            });
+                                                        } catch (Exception pe) {
+                                                            log.warn("右工位实时过程解析失败: {}", pe.getMessage());
+                                                        }
                                                     }
-                                                } else if (data.length() == 97 || data.length() == 98) {//检测结束
-                                                    log.info("检测结束");
-                                                    //结果
-                                                    String result = null;
-                                                    String yaLi = null;
-                                                    String yaJiang = null;
-                                                    String leakRate = null;
-                                                    String leakRateUnit = null;
+                                                } else if (InnomatecEndFrame.looksLikeEndFrame(data)) {
+                                                    log.info("右工位检测结束 rawLen={} data=[{}]", data.length(), data.replace("\r", "\\r").replace("\n", "\\n"));
                                                     try {
-                                                        result = data.substring(86, 98).trim();          // "PNE"(包含末尾的 E)
-                                                        yaLi = data.substring(35, 43).trim();            // "+ 224.58"(不变,已完整)
-                                                        String yaLiUnit = data.substring(45, 49).trim(); // "kPa"(不变)
-                                                        yaJiang = data.substring(51, 60).trim();         // "+ 0.00"(原缺最后一个 0,end+1)
-                                                        String yaJiangUnit = data.substring(61, 65).trim(); // "Pa"(不变)
-                                                        leakRate = data.substring(67, 76).trim();        // "+ 0.00"(原缺最后一个 0,end+1)
-                                                        leakRateUnit = data.substring(77, 85).trim();    // "ml/min"(原缺 n,end+1)
+                                                        InnomatecEndFrame end = InnomatecEndFrame.parse(data);
+                                                        log.info("右工位结束帧 frameLen={} result={} yaLi={} leak={}",
+                                                                end.frameLen, end.result, end.yaLi, end.leakRate);
+                                                        final String result = end.result;
+                                                        final String yaLi = end.yaLi;
+                                                        final String yaLiUnit = end.yaLiUnit;
+                                                        final String yaJiang = end.yaJiang;
+                                                        final String yaJiangUnit = end.yaJiangUnit;
+                                                        final String leakRate = end.leakRate;
+                                                        final String leakRateUnit = end.leakRateUnit;
+
+                                                        // 现场统一按泄漏率 ml/min(勿用压降 Pa)
+                                                        final String leakValue = leakRate;
+                                                        final String leakValueUnit = "ml/min";
+                                                        final String lastRet = InnomatecEndFrame.isPassResult(result) ? "OK" : "NG";
+                                                        final String sn = MesClient.product_sn2.getText().trim();
+                                                        final String deviceType = device;
 
                                                         MesClient.tjFlag2 = 3;
-//                                                    MesClient.setMenuStatus(MesClient.tjFlagText3,0);
-
-                                                        String leakValue = "";
-                                                        String leakValueUnit = "";
-                                                        if (MesClient.configParam.getLeakage2().equals("压降")) {
-                                                            leakValue = yaJiang;
-                                                            leakValueUnit = yaJiangUnit;
-                                                        } else {
-                                                            leakValue = leakRate;
-                                                            leakValueUnit = leakRateUnit;
-                                                        }
-
-                                                        MesClient.param3.setText(yaLi + yaLiUnit);
-                                                        MesClient.param4.setText(leakValue + leakRateUnit);
-                                                        String lastRet = "NG";
-                                                        if (result.equals("OK")) {
-                                                            MesClient.result2.setText("OK");
-                                                            MesClient.result2.setForeground(Color.GREEN);
-                                                            lastRet = "OK";
-                                                        } else {
-                                                            MesClient.result2.setText("NG");
-                                                            MesClient.result2.setForeground(Color.RED);
-                                                        }
-
-                                                        TestParam testParam = new TestParam();
-                                                        testParam.setSn(MesClient.product_sn2.getText().trim());
-                                                        testParam.setPressure(yaLi);
-                                                        testParam.setPressureUnit(yaLiUnit);
-                                                        testParam.setLeak(leakValue);
-                                                        testParam.setLeakUnit(leakValueUnit);
-                                                        testParam.setOprno(MesClient.configParam.getOprno2());
-                                                        testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle2());
-                                                        testParam.setProgramNo(MesClient.configParam.getProgramNo2());
-                                                        testParam.setUcode(MesClient.user_menu.getText().trim());
-                                                        testParam.setResult(lastRet);
-                                                        if (lastRet.equals("NG")) {
-                                                            testParam.setRemark(result);
-                                                        }else {
-                                                            testParam.setRemark("");
-                                                        }
-
-                                                        String testDate = DateLocalUtils.getCurrentTime();
-                                                        testParam.setCreateTime(testDate);
-                                                        JdbcUtils.insertTestRecord(testParam);
-
-                                                        if (MesClient.configParam.getPrintLabel().equals("是") && lastRet.equals("OK")) {
-                                                            // 打印查询最新的一条总成的数据
-                                                            TestParam testParam2 = JdbcUtils.getTestBySn(testParam.getSn(), testParam.getCreateTime());
-                                                            if (testParam2 != null && testParam2.getResult().equals("OK")) {
-                                                                PrintUtil.printLabel(testParam.getSn(), testParam2.getLeak() + testParam2.getLeakUnit(), testParam.getLeak() + testParam.getLeakUnit(), testDate);
+                                                        MesClient.work_status2 = 0;
+                                                        MesClient.mesStartFlag2 = 0;
+
+                                                        javax.swing.SwingUtilities.invokeLater(() -> {
+                                                            MesClient.setMenuStatus("右工位设备运行结束", 0);
+                                                            MesClient.param3.setText(yaLi + yaLiUnit);
+                                                            MesClient.param4.setText(leakValue + leakValueUnit);
+                                                            if ("OK".equals(lastRet)) {
+                                                                MesClient.result2.setText("OK");
+                                                                MesClient.result2.setForeground(Color.GREEN);
+                                                            } else {
+                                                                MesClient.result2.setText("NG");
+                                                                MesClient.result2.setForeground(Color.RED);
                                                             }
-                                                        }
-
-//                                                        JSONObject retObj = DataUtil.qmresult("500002", testParam);
-                                                        JSONObject retObj = DataUtil.qmResultData(testParam);
-                                                        if (retObj == null || retObj.get("result") == null) {
-                                                            MesClient.setMenuStatus("液冷测试结果上传失败,请重试", -1);
-                                                            return;
-                                                        }
-                                                        if (retObj.get("result").toString().equalsIgnoreCase("true")) {
-                                                            MesClient.work_status2 = 0;
-                                                            MesClient.setMenuStatus("液冷测试结果上传成功,请扫下一件", 0);
-                                                        } else {
-                                                            MesClient.setMenuStatus("液冷测试结果上传失败,请重试", -1);
-                                                            return;
-                                                        }
-
-//                                                    MesClient.getUser();
-//                                                    Boolean sendret = DataUtil.sendQmQuality(MesClient.nettyClient,testParam.getSn(),lastRet,MesClient.user20);
-//                                                    if(!sendret){
-//                                                        MesClient.setMenuStatus("消息发送失败,请重试",-1);
-//                                                        return;
-//                                                    }
+                                                            MesClient.result2.repaint();
+                                                            MesClient.param3.repaint();
+                                                            MesClient.param4.repaint();
 
-//                                                    MesClient.resetScanA();
-//                                                    MesClient.scan_type = 1;
-//                                                    MesClient.scanBarcode();
+                                                            if (MesClient.leakCheckRunningR) {
+                                                                MesClient.finishLeakCheck(1, yaLi, yaLiUnit, leakRate, leakRateUnit, deviceType);
+                                                                return;
+                                                            }
 
+                                                            TestParam testParam = new TestParam();
+                                                            testParam.setSn(sn);
+                                                            testParam.setPressure(yaLi);
+                                                            testParam.setPressureUnit(yaLiUnit);
+                                                            testParam.setLeak(leakValue);
+                                                            testParam.setLeakUnit(leakValueUnit);
+                                                            testParam.setOprno(MesClient.configParam.getOprno2());
+                                                            testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle2());
+                                                            testParam.setProgramNo(MesClient.configParam.getProgramNo2());
+                                                            testParam.setUcode(MesClient.user_menu.getText().trim());
+                                                            testParam.setResult(lastRet);
+                                                            testParam.setRemark("OK".equals(lastRet) ? "" : result);
+                                                            String testDate = DateLocalUtils.getCurrentTime();
+                                                            testParam.setCreateTime(testDate);
+                                                            testParam.setLineSn(MesClient.configParam.getLineSn());
+                                                            testParam.setDeviceType(deviceType);
+                                                            if (QmUploadUtil.saveAndUpload(testParam, false)) {
+                                                                MesClient.resetScanB();
+                                                                MesClient.setMenuStatus("右工位测试结果已保存,请扫下一件", 0);
+                                                            }
+                                                            if (MesClient.configParam.getPrintLabel() != null
+                                                                    && MesClient.configParam.getPrintLabel().equals("是")
+                                                                    && "OK".equals(lastRet)) {
+                                                                try {
+                                                                    TestParam testParam2 = JdbcUtils.getTestBySn(testParam.getSn(), testParam.getCreateTime());
+                                                                    if (testParam2 != null && "OK".equals(testParam2.getResult())) {
+                                                                        PrintUtil.printLabel(testParam.getSn(),
+                                                                                testParam2.getLeak() + testParam2.getLeakUnit(),
+                                                                                testParam.getLeak() + testParam.getLeakUnit(),
+                                                                                testDate);
+                                                                    }
+                                                                } catch (Exception pe) {
+                                                                    log.error("打印报错", pe);
+                                                                }
+                                                            }
+                                                        });
                                                     } catch (Exception e) {
-                                                        log.info(e.getMessage());
+                                                        log.error("右工位结束帧解析失败 rawLen=" + data.length(), e);
                                                     }
-
-                                                } else if (data.length() == 165 || data.contains("PARA_")) {//
-//                                                    String cq = data.substring(8, 13).trim();//充气
-//                                                    String by = data.substring(13, 18).trim();//保压
-//                                                    String cs = data.substring(18, 23).trim();//测试
-//                                                    MesClient.bao_ya_text.setText(by + "(s)");
-//                                                    MesClient.chong_qi_text.setText(cq + "(s)");
-//                                                    MesClient.ce_shi_text.setText(cs + "(s)");
                                                 }
                                             }
-                                            if (MesClient.tjFlag == 3) {
-//
-                                            }
                                         }
                                     } else {
 //                                    sendData("STOP");
@@ -360,4 +340,38 @@ public class SerialPortUtils2 {
 
     }
 
+    public boolean changeProgram() {
+        return changeProgram(MesClient.configParam != null ? MesClient.configParam.getProgramNo2() : "1");
+    }
+
+    public boolean changeProgram(String programNoStr) {
+        if (isOpen) {
+            int programNoVal;
+            try {
+                programNoVal = Integer.parseInt(programNoStr == null || programNoStr.trim().isEmpty() ? "1" : programNoStr.trim());
+            } catch (Exception e) {
+                programNoVal = 1;
+            }
+            if (device.equals("ateq")) {
+                String s1 = DataUtils.CRC16(ATEQ2.getStationNo() + " 10 02 00 00 01 02 " + DataUtils.intToWord(programNoVal - 1));
+                log.info("切换程序{} -> {}", programNoVal, s1);
+                byte[] read = this.read(s1);
+                String s2 = DataUtils.CRC16(ATEQ2.getStationNo() + " 10 30 04 00 01");
+                boolean result = Arrays.equals(read, DataUtils.hexStringToByteArray(s2));
+                log.info("切换程序{}", result ? "成功" : "失败");
+                return result;
+            } else if (device.equals("innomatec")) {
+                String readProgramNo = "PRG_" + String.format("%03d", programNoVal);
+                this.sendData(readProgramNo);
+                return true;
+            }
+            return true;
+        } else {
+            log.info("成品气密仪串口未打开,尝试重新打开串口...");
+            MesClient.setMenuStatus("成品气密仪连接失败,请检查设备后重试", 1);
+            isOpen = openPort();
+            return false;
+        }
+    }
+
 }

+ 133 - 134
src/com/mes/util/WorkTimer.java

@@ -1,9 +1,6 @@
 package com.mes.util;
 
-import com.alibaba.fastjson2.JSONObject;
-import com.mes.ui.DataUtil;
 import com.mes.ui.MesClient;
-import com.mes.util.TestParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -11,25 +8,31 @@ import java.awt.*;
 import java.util.Timer;
 import java.util.TimerTask;
 
+/**
+ * 左工位 ateq 轮询(对齐 qm-new-2:先读最终结果并刷新 OK/NG,再复位;读失败保留 work_status 以便重试)
+ */
 public class WorkTimer {
     private static final Logger log = LoggerFactory.getLogger(WorkTimer.class);
 
-    private static Timer stopTimer = null;
     private static Timer workTimer = null;
 
     private WorkTimer() {
     }
 
     public static void start() {
-        if(workTimer != null){
+        if (workTimer != null) {
+            workTimer.cancel();
             workTimer = null;
         }
         workTimer = new Timer();
-        // 左工位轮询
         workTimer.schedule(new WorkTimerTask(), 1000, 2000);
+    }
 
-//        stopTimer = new Timer();
-//        stopTimer.schedule(new WorkTimer.StopTimerTask(), 1000, 5000);
+    /** 已在跑则不重启,避免点「启动」时打断轮询 */
+    public static void ensureStarted() {
+        if (workTimer == null) {
+            start();
+        }
     }
 
     public static void stop() {
@@ -37,145 +40,141 @@ public class WorkTimer {
             workTimer.cancel();
             workTimer = null;
         }
-        if (stopTimer != null) {
-            stopTimer.cancel();
-            stopTimer = null;
-        }
     }
 
-
-    private static class StopTimerTask extends TimerTask {
+    private static class WorkTimerTask extends TimerTask {
         @Override
         public void run() {
             try {
-                // 如果不在工作状态,或者质检没通过,强制发送停止指令
-                if (MesClient.work_status == 0 || !MesClient.check_quality_result) {
-                    ATEQ.stop();
+                if (MesClient.work_status != 1) {
+                    return;
+                }
+                ATEQ.RealtimeResult realtimeResult = ATEQ.readRealtimeResult();
+                log.info(realtimeResult.toString());
+                boolean isEnd = false;
+                if (realtimeResult.stepCode == 65535) {
+                    if (MesClient.mesStartFlag == 1) {
+                        isEnd = true;
+                    }
+                } else {
+                    MesClient.mesStartFlag = 1;
+                    MesClient.setMenuStatus("左工位当前步骤:[" + realtimeResult.stepCode + "]" + realtimeResult.step, 0);
+                }
+
+                final String pText = isEnd ? "" : realtimeResult.pressureValue + " " + realtimeResult.pressureUnit;
+                final String lText = isEnd ? "" : realtimeResult.leakValue + " ml/min";
+                javax.swing.SwingUtilities.invokeLater(() -> {
+                    MesClient.param1.setText(pText);
+                    MesClient.param2.setText(lText);
+                });
+
+                if (!isEnd) {
+                    return;
+                }
+
+                // 对齐 qm:立即结束工作态,避免轮询重复判定覆盖结果
+                MesClient.work_status = 0;
+                MesClient.mesStartFlag = 0;
+
+                double realtimeLeak = realtimeResult.leakValue;
+                ATEQ.LastResult lastResult = ATEQ.readLastResult();
+                log.info(lastResult.toString());
+
+                double leakValue;
+                final String pressureText;
+                final String leakText;
+                if (MesClient.leakCheckRunningL) {
+                    leakValue = realtimeLeak;
+                    if (leakValue == 0 && lastResult.leakValue != 0) {
+                        leakValue = lastResult.leakValue;
+                    }
+                    pressureText = lastResult.pressureValue + lastResult.pressureUnit;
+                    leakText = leakValue + " ml/min";
+                    log.info("标漏采用泄漏率判定值={}, 实时={}, 最终={}", leakValue, realtimeLeak, lastResult.leakValue);
+                } else {
+                    leakValue = lastResult.leakValue;
+                    pressureText = lastResult.pressureValue + lastResult.pressureUnit;
+                    if (leakValue == 0 && !lastResult.isPass) {
+                        leakText = "9999 ml/min";
+                        leakValue = 9999;
+                    } else {
+                        leakText = lastResult.leakValue + " ml/min";
+                    }
                 }
+
+                final double leakValueFinal = leakValue;
+                final ATEQ.LastResult lastResultFinal = lastResult;
+                javax.swing.SwingUtilities.invokeLater(() -> {
+                    MesClient.param1.setText(pressureText);
+                    MesClient.param2.setText(leakText);
+                    MesClient.param1.repaint();
+                    MesClient.param2.repaint();
+                    // 必须用左侧 running 标志,不能用全局 leakCheckMode(右侧标漏时左侧气密会误判)
+                    if (MesClient.leakCheckRunningL) {
+                        String deviceType = MesClient.configParam.getDevice() != null ? MesClient.configParam.getDevice() : "ateq";
+                        MesClient.finishLeakCheck(
+                                0,
+                                lastResultFinal.pressureValue + "",
+                                lastResultFinal.pressureUnit,
+                                String.valueOf(leakValueFinal),
+                                "ml/min",
+                                deviceType);
+                    } else {
+                        handleQmEnd(lastResultFinal, leakValueFinal);
+                    }
+                });
+                return;
             } catch (Exception e) {
-                 log.error("禁止运行任务报错"); // 防止日志刷屏可注释
+                e.printStackTrace();
+                log.error("工作定时任务报错:" + e.getMessage());
             }
         }
-    }
 
-    // 工作流程定时任务
-    private static class WorkTimerTask extends TimerTask {
-        @Override
-        public void run() {
-            try {
-                // 如果开始工作 (work_status == 1)
-                if (MesClient.work_status == 1) {
-
-                    // 1. 读取实时结果
-                    ATEQ.RealtimeResult realtimeResult = ATEQ.readRealtimeResult();
-
-                    Boolean isEnd = false;
-                    //代码核心判断逻辑:通过步序码 65535 判断结束
-                    if (realtimeResult.stepCode == 65535) {
-                        if (MesClient.mesStartFlag == 1) {
-                            isEnd = true;
-                        }
-                    } else {
-                        MesClient.mesStartFlag = 1; // 标记设备已经开始运行
-                        MesClient.setMenuStatus("左工位当前步骤:[" + realtimeResult.stepCode + "]" + realtimeResult.step, 0);
-                    }
+        private void handleQmEnd(ATEQ.LastResult lastResult, double leakValue) {
+            String lastRet = lastResult.isPass ? "OK" : "NG";
+            if (leakValue == 9999) {
+                lastRet = "NG";
+            }
+
+            if ("OK".equals(lastRet)) {
+                MesClient.result1.setText("OK");
+                MesClient.result1.setForeground(Color.GREEN);
+            } else {
+                MesClient.result1.setText("NG");
+                MesClient.result1.setForeground(Color.RED);
+            }
 
-                    System.out.println("压力值:"+realtimeResult.pressureValue);
-                    System.out.println("泄露值:"+realtimeResult.leakValue);
-                    // 实时更新 UI (如果是结束瞬间,清空显示,为了后面显示最终结果)
-                    MesClient.param1.setText(isEnd ? "" : realtimeResult.pressureValue + " " + realtimeResult.pressureUnit);
-                    MesClient.param2.setText(isEnd ? "" : realtimeResult.leakValue + " " + realtimeResult.leakUnit);
-
-                    // 2. 如果判断测试结束
-                    if (isEnd) {
-                        log.info("左工位检测结束,读取最终结果...");
-
-                        // 读取最终结果
-                        ATEQ.LastResult lastResult = ATEQ.readLastResult();
-                        log.info(lastResult.toString());
-
-                        // 更新压力显示
-                        MesClient.param1.setText(lastResult.pressureValue + lastResult.pressureUnit);
-
-                        // 如果泄漏值为0且不合格,显示9999
-                        if (lastResult.leakValue == 0 && !lastResult.isPass) {
-                            MesClient.param2.setText("9999");
-                        } else {
-                            MesClient.param2.setText(lastResult.leakValue + lastResult.leakUnit);
-                        }
-
-                        String lastRet = lastResult.isPass ? "OK" : "NG";
-
-                        // 更新左工位结果 UI (result1)
-                        if (lastRet.equals("OK")) {
-                            MesClient.result1.setText("OK");
-                            MesClient.result1.setForeground(Color.GREEN);
-                        } else {
-                            MesClient.result1.setText("NG");
-                            MesClient.result1.setForeground(Color.RED);
-                        }
-
-                        // 3. 构建保存数据
-                        String testDate = DateLocalUtils.getCurrentTime();
-                        TestParam testParam = new TestParam();
-
-                        // 使用左工位条码框 product_sn1
-                        testParam.setSn(MesClient.product_sn1.getText().trim());
-                        testParam.setUcode(MesClient.user_menu.getText().trim());
-                        testParam.setLineSn(MesClient.configParam.getLineSn());
-                        testParam.setOprno(MesClient.configParam.getOprno());
-                        testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle());
-
-                        // 适配 TestParam 字段 (根据您的实体类调整,这里用通用命名)
-                        testParam.setPressure(String.valueOf(lastResult.pressureValue));     // 对应 param1
-                        testParam.setPressureUnit(lastResult.pressureUnit);                  // 对应 param2
-                        testParam.setLeak(String.valueOf(lastResult.leakValue));             // 对应 param3
-                        testParam.setLeakUnit(lastResult.leakUnit);                          // 对应 param4
-
-                        testParam.setProgramNo(MesClient.programNo.getText().trim()); // 对应 param5
-                        testParam.setResult(lastRet);
-                        testParam.setDeviceType("ateq");
-                        testParam.setRemark("");
-                        testParam.setCreateTime(testDate);
-
-                        // 插入本地库
-                        JdbcUtils.insertTestRecord(testParam);
-
-                        // 4. 上传 MES
-                        if (MesClient.mes_enable) { // 强制开启或根据开关
-                            JSONObject retObj = DataUtil.qmResultData(testParam);
-                            if (retObj != null && retObj.get("result") != null && retObj.get("result").toString().equalsIgnoreCase("true")) {
-                                MesClient.resetScanA(); // 重置界面
-                                MesClient.setMenuStatus("左工位测试结果上传成功,请扫下一件", 0);
-                            } else {
-                                MesClient.setMenuStatus("左工位测试结果上传失败,请重试", -1);
-                                return; // 失败不重置,保留现场
-                            }
-                        } else {
-                            //重置启动位
-                            MesClient.resetScanA();
-                        }
-
-                        // 5. 打印标签 (老代码逻辑)
-                        if (MesClient.configParam.getPrintLabel().equals("是") && lastRet.equals("OK")) {
-                            try {
-                                PrintUtil.printLabel(
-                                        testParam.getSn(),
-                                        testParam.getPressure() + testParam.getPressureUnit(),
-                                        testParam.getLeak() + testParam.getLeakUnit(),
-                                        testDate
-                                );
-                            } catch (Exception e) {
-                                log.error("打印报错", e);
-                            }
-                        }
+            TestParam testParam = new TestParam();
+            testParam.setSn(MesClient.product_sn1.getText().trim());
+            testParam.setUcode(MesClient.user_menu.getText().trim());
+            testParam.setLineSn(MesClient.configParam.getLineSn());
+            testParam.setOprno(MesClient.configParam.getOprno());
+            testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle());
+            testParam.setPressure(lastResult.pressureValue + "");
+            testParam.setPressureUnit(lastResult.pressureUnit);
+            testParam.setLeak(lastResult.leakValue + "");
+            testParam.setLeakUnit("ml/min");
+            testParam.setProgramNo(LeakCheckUtil.qmProgramNoForSide(0));
+            testParam.setResult(lastRet);
+            testParam.setDeviceType("ateq");
+            testParam.setRemark("");
+            testParam.setCreateTime(DateLocalUtils.getCurrentTime());
+            if (QmUploadUtil.saveAndUpload(testParam, false)) {
+                MesClient.resetScanA();
+                MesClient.setMenuStatus("左工位测试结果已保存,请扫下一件", 0);
+            }
 
-                    }
+            if ("是".equals(MesClient.configParam.getPrintLabel()) && "OK".equals(lastRet)) {
+                try {
+                    PrintUtil.printLabel(
+                            testParam.getSn(),
+                            testParam.getPressure() + testParam.getPressureUnit(),
+                            testParam.getLeak() + testParam.getLeakUnit(),
+                            testParam.getCreateTime());
+                } catch (Exception e) {
+                    log.error("打印报错", e);
                 }
-            } catch (Exception e) {
-                 e.printStackTrace();
-                 log.error("工作定时任务报错:" + e.getMessage());
             }
         }
     }
-}
+}

+ 119 - 121
src/com/mes/util/WorkTimer2.java

@@ -1,9 +1,6 @@
 package com.mes.util;
 
-import com.alibaba.fastjson2.JSONObject;
-import com.mes.ui.DataUtil;
 import com.mes.ui.MesClient;
-import com.mes.util.TestParam;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -11,10 +8,12 @@ import java.awt.*;
 import java.util.Timer;
 import java.util.TimerTask;
 
+/**
+ * 右工位 ateq 轮询(对齐 qm-new-2)
+ */
 public class WorkTimer2 {
     private static final Logger log = LoggerFactory.getLogger(WorkTimer2.class);
 
-    private static Timer stopTimer = null;
     private static Timer workTimer = null;
 
     private WorkTimer2() {
@@ -22,15 +21,17 @@ public class WorkTimer2 {
 
     public static void start() {
         if (workTimer != null) {
+            workTimer.cancel();
             workTimer = null;
         }
-
         workTimer = new Timer();
-        // 右工位轮询
         workTimer.schedule(new WorkTimerTask(), 1000, 2000);
-//
-//        stopTimer = new Timer();
-//        stopTimer.schedule(new StopTimerTask(), 1000, 5000);
+    }
+
+    public static void ensureStarted() {
+        if (workTimer == null) {
+            start();
+        }
     }
 
     public static void stop() {
@@ -38,134 +39,131 @@ public class WorkTimer2 {
             workTimer.cancel();
             workTimer = null;
         }
-        if (stopTimer != null) {
-            stopTimer.cancel();
-            stopTimer = null;
-        }
     }
 
-    private static class StopTimerTask extends TimerTask {
+    private static class WorkTimerTask extends TimerTask {
         @Override
         public void run() {
             try {
-                // 注意:这里检查 work_status2 (右工位状态)
-                if (MesClient.work_status2 == 0) {
-                    ATEQ2.stop(); // 调用右工位专属的停止指令
+                if (MesClient.work_status2 != 1) {
+                    return;
+                }
+                ATEQ2.RealtimeResult realtimeResult = ATEQ2.readRealtimeResult();
+                log.info(realtimeResult.toString());
+                boolean isEnd = false;
+                if (realtimeResult.stepCode == 65535) {
+                    if (MesClient.mesStartFlag2 == 1) {
+                        isEnd = true;
+                    }
+                } else {
+                    MesClient.mesStartFlag2 = 1;
+                    MesClient.setMenuStatus("右工位当前步骤:[" + realtimeResult.stepCode + "]" + realtimeResult.step, 0);
                 }
-            } catch (Exception e) {
-            }
-        }
-    }
 
-    private static class WorkTimerTask extends TimerTask {
-        @Override
-        public void run() {
-            try {
-                // 检查右工位状态 work_status2
-                if (MesClient.work_status2 == 1) {
+                MesClient.param3.setText(isEnd ? "" : realtimeResult.pressureValue + " " + realtimeResult.pressureUnit);
+                MesClient.param4.setText(isEnd ? "" : realtimeResult.leakValue + " ml/min");
 
+                if (!isEnd) {
+                    return;
+                }
 
-                    System.out.println("开始读取右工位数据...");
-                    // 调用 ATEQ2 读取数据
-                    ATEQ2.RealtimeResult realtimeResult = ATEQ2.readRealtimeResult();
+                log.info("右工位检测结束...");
+                // 对齐 qm:立即结束工作态,避免轮询重复判定覆盖结果
+                MesClient.work_status2 = 0;
+                MesClient.mesStartFlag2 = 0;
+
+                double realtimeLeak = realtimeResult.leakValue;
+                ATEQ2.LastResult lastResult = ATEQ2.readLastResult();
+                log.info(lastResult.toString());
+                MesClient.param3.setText(lastResult.pressureValue + lastResult.pressureUnit);
+
+                double leakValue;
+                if (MesClient.leakCheckRunningR) {
+                    leakValue = realtimeLeak;
+                    if (leakValue == 0 && lastResult.leakValue != 0) {
+                        leakValue = lastResult.leakValue;
+                    }
+                    MesClient.param4.setText(leakValue + " ml/min");
+                    log.info("标漏采用泄漏率判定值={}, 实时={}, 最终={}", leakValue, realtimeLeak, lastResult.leakValue);
+                } else {
+                    leakValue = lastResult.leakValue;
+                    if (leakValue == 0 && !lastResult.isPass) {
+                        MesClient.param4.setText("9999 ml/min");
+                        leakValue = 9999;
+                    } else {
+                        MesClient.param4.setText(lastResult.leakValue + " ml/min");
+                    }
+                }
 
-                    System.out.println("压力值:"+realtimeResult.pressureValue);
-                    System.out.println("泄露值:"+realtimeResult.leakValue);
+                // 必须用右侧 running 标志,不能用全局 leakCheckMode
+                if (MesClient.leakCheckRunningR) {
+                    final double judgeVal = leakValue;
+                    String deviceType = MesClient.configParam.getDevice2() != null ? MesClient.configParam.getDevice2() : "ateq";
+                    javax.swing.SwingUtilities.invokeLater(() ->
+                            MesClient.finishLeakCheck(
+                                    1,
+                                    lastResult.pressureValue + "",
+                                    lastResult.pressureUnit,
+                                    String.valueOf(judgeVal),
+                                    "ml/min",
+                                    deviceType));
+                } else {
+                    handleQmEnd(lastResult, leakValue);
+                }
+            } catch (Exception e) {
+                log.error("右工位轮询报错", e);
+            }
+        }
 
+        private void handleQmEnd(ATEQ2.LastResult lastResult, double leakValue) {
+            String lastRet = lastResult.isPass ? "OK" : "NG";
+            if (leakValue == 9999) {
+                lastRet = "NG";
+            }
 
-                    System.out.println("查看获取到的数据"+realtimeResult.toString());
-                    Boolean isEnd = false;
-                    // 老代码的核心判断逻辑:通过步序码 65535 判断结束
-                    if (realtimeResult.stepCode == 65535) {
-                        if (MesClient.mesStartFlag2 == 1) {
-                            isEnd = true;
-                        }
-                    } else {
-                        MesClient.mesStartFlag2 = 1; // 标记设备已经开始运行
-                        MesClient.setMenuStatus("右工位当前步骤:[" + realtimeResult.stepCode + "]" + realtimeResult.step, 0);
-                    }
+            if ("OK".equals(lastRet)) {
+                MesClient.result2.setText("OK");
+                MesClient.result2.setForeground(Color.GREEN);
+            } else {
+                MesClient.result2.setText("NG");
+                MesClient.result2.setForeground(Color.RED);
+            }
 
+            String testDate = DateLocalUtils.getCurrentTime();
+            TestParam testParam = new TestParam();
+            testParam.setSn(MesClient.product_sn2.getText().trim());
+            testParam.setUcode(MesClient.user_menu.getText().trim());
+            testParam.setLineSn(MesClient.configParam.getLineSn());
+            testParam.setOprno(MesClient.configParam.getOprno2());
+            testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle2());
+            testParam.setPressure(String.valueOf(lastResult.pressureValue));
+            testParam.setPressureUnit(lastResult.pressureUnit);
+            testParam.setLeak(String.valueOf(lastResult.leakValue));
+            testParam.setLeakUnit("ml/min");
+            testParam.setProgramNo(LeakCheckUtil.qmProgramNoForSide(1));
+            testParam.setResult(lastRet);
+            testParam.setCreateTime(testDate);
+            testParam.setDeviceType("ateq");
+            testParam.setRemark("");
+            if (QmUploadUtil.saveAndUpload(testParam, false)) {
+                MesClient.resetScanB();
+                MesClient.setMenuStatus("右工位测试结果已保存,请扫下一件", 0);
+            }
 
-                    System.out.println("压力值:"+realtimeResult.pressureValue);
-                    System.out.println("泄露值:"+realtimeResult.leakValue);
-
-                    // 更新右工位 UI (param3, param4)
-                    MesClient.param3.setText(isEnd ? "" : realtimeResult.pressureValue + " " + realtimeResult.pressureUnit);
-                    MesClient.param4.setText(isEnd ? "" : realtimeResult.leakValue + " " + realtimeResult.leakUnit);
-
-                    if (isEnd) {
-                        log.info("右工位检测结束...");
-                        ATEQ2.LastResult lastResult = ATEQ2.readLastResult();
-
-                        MesClient.param3.setText(lastResult.pressureValue + lastResult.pressureUnit);
-                        MesClient.param4.setText(lastResult.leakValue + lastResult.leakUnit);
-
-                        String lastRet = lastResult.isPass ? "OK" : "NG";
-
-                        // 更新右工位结果 result2
-                        if (lastRet.equals("OK")) {
-                            MesClient.result2.setText("OK");
-                            MesClient.result2.setForeground(Color.GREEN);
-                        } else {
-                            MesClient.result2.setText("NG");
-                            MesClient.result2.setForeground(Color.RED);
-                        }
-
-                        // 保存数据 (使用 product_sn2)
-                        String testDate = DateLocalUtils.getCurrentTime();
-                        TestParam testParam = new TestParam();
-                        testParam.setSn(MesClient.product_sn2.getText().trim());
-                        testParam.setUcode(MesClient.user_menu.getText().trim());
-                        testParam.setLineSn(MesClient.configParam.getLineSn());
-                        testParam.setOprno(MesClient.configParam.getOprno2()); // 右工位编号
-                        testParam.setOprnoTitle(MesClient.configParam.getOprnoTitle2()); // 右工位名
-                        testParam.setPressure(String.valueOf(lastResult.pressureValue));
-                        testParam.setPressureUnit(lastResult.pressureUnit);
-                        testParam.setLeak(String.valueOf(lastResult.leakValue));
-                        testParam.setLeakUnit(lastResult.leakUnit);
-                        testParam.setProgramNo(MesClient.programNo2.getText().trim());
-                        testParam.setResult(lastRet);
-                        testParam.setCreateTime(testDate);
-                        testParam.setDeviceType("ateq");
-                        testParam.setRemark("");
-                        JdbcUtils.insertTestRecord(testParam);
-
-                        // 打印逻辑 (打印双工位关联数据)
-                        if (MesClient.configParam.getPrintLabel().equals("是") && lastRet.equals("OK")) {
-                            // 查询最新的左工位(总成)数据进行拼合打印
-                            TestParam testParamLeft = JdbcUtils.getTestBySn(testParam.getSn(), testParam.getCreateTime());
-                            if (testParamLeft != null && "OK".equals(testParamLeft.getResult())) {
-                                try {
-                                    PrintUtil.printLabel(
-                                            testParam.getSn(),
-                                            testParamLeft.getLeak() + testParamLeft.getLeakUnit(), // 左工位泄漏
-                                            testParam.getLeak() + testParam.getLeakUnit(),         // 右工位泄漏
-                                            testDate
-                                    );
-                                } catch (Exception e) {
-                                    log.error("打印报错", e);
-                                }
-                            }
-                        }
-
-                        // 上传 MES
-                        if (MesClient.mes_enable) {
-                            JSONObject retObj = DataUtil.qmResultData(testParam);
-                            if (retObj != null && "true".equalsIgnoreCase(String.valueOf(retObj.get("result")))) {
-                                MesClient.resetScanB(); // 重置右工位
-                                MesClient.setMenuStatus("右工位测试结果上传成功,请扫下一件", 0);
-                            } else {
-                                MesClient.setMenuStatus("右工位测试结果上传失败,请重试", -1);
-                                return;
-                            }
-                        } else {
-                            MesClient.resetScanB();
-                        }
+            if ("是".equals(MesClient.configParam.getPrintLabel()) && "OK".equals(lastRet)) {
+                TestParam testParamLeft = JdbcUtils.getTestBySn(testParam.getSn(), testParam.getCreateTime());
+                if (testParamLeft != null && "OK".equals(testParamLeft.getResult())) {
+                    try {
+                        PrintUtil.printLabel(
+                                testParam.getSn(),
+                                testParamLeft.getLeak() + testParamLeft.getLeakUnit(),
+                                testParam.getLeak() + testParam.getLeakUnit(),
+                                testDate);
+                    } catch (Exception e) {
+                        log.error("打印报错", e);
                     }
                 }
-            } catch (Exception e) {
-                 log.error("右工位轮询报错", e);
             }
         }
     }
-}
+}

+ 1 - 1
src/resources/config/config.properties

@@ -1,4 +1,4 @@
-mes.gw=OP330A
+mes.gw=OP385A
 mes.server_ip=127.0.0.1
 #mes.server_ip=192.168.15.99
 mes.tcp_port=3000