ソースを参照

添加设备自动上报,登录更新jar包,改为4枪版未测试

hfy 2 日 前
コミット
c6dcde73f9

+ 116 - 0
src/com/mes/device/DeviceStateReporter.java

@@ -0,0 +1,116 @@
+package com.mes.device;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.HttpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Properties;
+
+/**
+ * 工位客户端设备状态上报(按 gw + lineSn)
+ */
+public class DeviceStateReporter {
+
+    public static final Logger log = LoggerFactory.getLogger(DeviceStateReporter.class);
+
+    /** 正常运行 */
+    public static final String STATE_RUNNING = "1";
+    /** 工位退出/收班 */
+    public static final String STATE_STOP = "7";
+
+    private static DeviceStateReporter instance;
+
+    private String serverIp;
+    private String gw;
+    private String lineSn;
+
+    private DeviceStateReporter() {
+    }
+
+    public static DeviceStateReporter getInstance() {
+        if (instance == null) {
+            synchronized (DeviceStateReporter.class) {
+                if (instance == null) {
+                    instance = new DeviceStateReporter();
+                }
+            }
+        }
+        return instance;
+    }
+
+    /**
+     * 从配置文件初始化
+     */
+    public void initFromConfig() {
+        try {
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+            pro.load(br);
+            br.close();
+            is.close();
+            init(
+                    pro.getProperty("mes.server_ip", "").trim(),
+                    pro.getProperty("mes.gw", "").trim(),
+                    pro.getProperty("mes.line_sn", "").trim()
+            );
+        } catch (Exception e) {
+            log.error("初始化设备状态上报失败", e);
+        }
+    }
+
+    public void init(String serverIp, String gw, String lineSn) {
+        this.serverIp = serverIp;
+        this.gw = gw;
+        this.lineSn = lineSn;
+        log.info("设备状态上报初始化: gw={}, lineSn={}", gw, lineSn);
+    }
+
+    /**
+     * 异步上报
+     */
+    public void reportAsync(final String state, final String content) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                reportSync(state, content);
+            }
+        }, "device-state-report-thread").start();
+    }
+
+    /**
+     * 同步上报
+     */
+    public boolean reportSync(String state, String content) {
+        if (isBlank(serverIp) || isBlank(gw) || isBlank(lineSn)) {
+            log.warn("设备状态上报跳过:配置不完整 serverIp={}, gw={}, lineSn={}", serverIp, gw, lineSn);
+            return false;
+        }
+        try {
+            String url = "http://" + serverIp + ":8980/js/a/mes/mesDevice/reportState";
+            JSONObject body = new JSONObject();
+            body.put("gw", gw);
+            body.put("lineSn", lineSn);
+            body.put("state", state);
+            body.put("content", content == null ? "" : content);
+            String result = HttpUtils.sendPostRequestJson(url, body.toJSONString());
+            log.info("设备状态上报: state={}, content={}, result={}", state, content, result);
+            if (isBlank(result) || "false".equalsIgnoreCase(result)) {
+                return false;
+            }
+            JSONObject retObj = JSONObject.parseObject(result);
+            return retObj != null && "true".equalsIgnoreCase(String.valueOf(retObj.get("result")));
+        } catch (Exception e) {
+            log.error("设备状态上报异常: state={}, content={}", state, content, e);
+            return false;
+        }
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

+ 180 - 145
src/com/mes/ui/DeviceManagePanel.java

@@ -11,88 +11,75 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 设备管理:IP、预设数量、按颗数区间的铆接参数配置
+ * Device settings: PLC IP, preset count, and rivet parameter ranges for A/B/C/D guns.
  */
 public class DeviceManagePanel extends JPanel {
 
-    private static final String[] PARAM_COLUMNS = {"起始颗", "结束颗", "F-min", "F-max", "S-min", "S-max"};
+    private static final String[] GUN_TYPES = {"A", "B", "C", "D"};
+    private static final String[] PARAM_COLUMNS = {"\u8d77\u59cb\u9897", "\u7ed3\u675f\u9897", "F-min", "F-max", "S-min", "S-max"};
     private static final int CONTENT_WIDTH = 940;
+    private static final int TABLE_HEIGHT = 150;
+    private static final int GUN_BLOCK_HEIGHT = 232;
 
-    private JTextField txtASet;
-    private JTextField txtBSet;
-    private JTextField txtIpA;
-    private JTextField txtIpB;
-    private DefaultTableModel tableModelA;
-    private DefaultTableModel tableModelB;
-    private JTable tableA;
-    private JTable tableB;
+    private JTextField[] txtSets = new JTextField[GUN_TYPES.length];
+    private JTextField[] txtIps = new JTextField[GUN_TYPES.length];
+    private DefaultTableModel[] tableModels = new DefaultTableModel[GUN_TYPES.length];
+    private JTable[] tables = new JTable[GUN_TYPES.length];
 
     public DeviceManagePanel() {
         setLayout(null);
-        setPreferredSize(new Dimension(980, 720));
+        setPreferredSize(new Dimension(980, 1220));
         buildUi();
         loadConfigToUi();
     }
 
     private void buildUi() {
-        Font labelFont = new Font("微软雅黑", Font.PLAIN, 18);
-        Font fieldFont = new Font("微软雅黑", Font.PLAIN, 18);
-        Font hintFont = new Font("微软雅黑", Font.PLAIN, 14);
-
-        addLabel("A枪预设数量:", 20, 16, 130, 34, labelFont);
-        txtASet = addField(String.valueOf(MesClient.aSetNum), 150, 16, 120, 34, fieldFont);
-
-        addLabel("B枪预设数量:", 320, 16, 130, 34, labelFont);
-        txtBSet = addField(String.valueOf(MesClient.bSetNum), 450, 16, 120, 34, fieldFont);
-
-        addLabel("A枪IP:", 20, 58, 80, 34, labelFont);
-        txtIpA = addField(MesClient.curIpA, 100, 58, 170, 34, fieldFont);
-
-        addLabel("B枪IP:", 320, 58, 80, 34, labelFont);
-        txtIpB = addField(MesClient.curIpB, 400, 58, 170, 34, fieldFont);
-
-        addLabel("说明:S-min/S-max 为PLC原始值,界面行程显示值 = S / 1000", 20, 98, CONTENT_WIDTH, 24, hintFont);
-
-        addLabel("A枪参数区间", 20, 128, 200, 28, labelFont);
-        tableModelA = createParamTableModel();
-        tableA = createParamTable(tableModelA, 20, 156, CONTENT_WIDTH, 150);
-
-        JButton btnAddA = new JButton("A枪添加行");
-        btnAddA.setFont(labelFont);
-        btnAddA.setBounds(20, 316, 120, 32);
-        btnAddA.addActionListener(e -> tableModelA.addRow(new Object[]{"", "", "0", "0", "0", "0"}));
-        add(btnAddA);
-
-        JButton btnDelA = new JButton("A枪删除行");
-        btnDelA.setFont(labelFont);
-        btnDelA.setBounds(150, 316, 120, 32);
-        btnDelA.addActionListener(e -> removeSelectedRow(tableA, tableModelA));
-        add(btnDelA);
-
-        addLabel("B枪参数区间", 20, 360, 200, 28, labelFont);
-        tableModelB = createParamTableModel();
-        tableB = createParamTable(tableModelB, 20, 388, CONTENT_WIDTH, 150);
-
-        JButton btnAddB = new JButton("B枪添加行");
-        btnAddB.setFont(labelFont);
-        btnAddB.setBounds(20, 548, 120, 32);
-        btnAddB.addActionListener(e -> tableModelB.addRow(new Object[]{"", "", "0", "0", "0", "0"}));
-        add(btnAddB);
-
-        JButton btnDelB = new JButton("B枪删除行");
-        btnDelB.setFont(labelFont);
-        btnDelB.setBounds(150, 548, 120, 32);
-        btnDelB.addActionListener(e -> removeSelectedRow(tableB, tableModelB));
-        add(btnDelB);
-
-        JButton btnSave = new JButton("保存全部设置");
-        btnSave.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        btnSave.setBounds(380, 596, 180, 45);
+        Font labelFont = new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18);
+        Font fieldFont = new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18);
+        Font hintFont = new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 14);
+
+        for (int i = 0; i < GUN_TYPES.length; i++) {
+            int y = 16 + i * 42;
+            String gunType = GUN_TYPES[i];
+            addLabel(gunType + "\u67aa\u9884\u8bbe\u6570\u91cf\uff1a", 20, y, 130, 34, labelFont);
+            txtSets[i] = addField(String.valueOf(getSetNum(gunType)), 150, y, 120, 34, fieldFont);
+
+            addLabel(gunType + "\u67aaIP\uff1a", 320, y, 80, 34, labelFont);
+            txtIps[i] = addField(getIp(gunType), 400, y, 170, 34, fieldFont);
+        }
+
+        addLabel("\u8bf4\u660e\uff1aS-min/S-max \u4e3aPLC\u539f\u59cb\u503c\uff0c\u754c\u9762\u884c\u7a0b\u663e\u793a\u503c = S / 1000",
+                20, 184, CONTENT_WIDTH, 24, hintFont);
+
+        for (int i = 0; i < GUN_TYPES.length; i++) {
+            int blockY = 216 + i * GUN_BLOCK_HEIGHT;
+            String gunType = GUN_TYPES[i];
+            addLabel(gunType + "\u67aa\u53c2\u6570\u533a\u95f4", 20, blockY, 200, 28, labelFont);
+            tableModels[i] = createParamTableModel();
+            tables[i] = createParamTable(tableModels[i], 20, blockY + 28, CONTENT_WIDTH, TABLE_HEIGHT);
+
+            JButton btnAdd = new JButton(gunType + "\u67aa\u6dfb\u52a0\u884c");
+            btnAdd.setFont(labelFont);
+            btnAdd.setBounds(20, blockY + 188, 120, 32);
+            final int tableIndex = i;
+            btnAdd.addActionListener(e -> tableModels[tableIndex].addRow(new Object[]{"", "", "0", "0", "0", "0"}));
+            add(btnAdd);
+
+            JButton btnDel = new JButton(gunType + "\u67aa\u5220\u9664\u884c");
+            btnDel.setFont(labelFont);
+            btnDel.setBounds(150, blockY + 188, 120, 32);
+            btnDel.addActionListener(e -> removeSelectedRow(tables[tableIndex], tableModels[tableIndex]));
+            add(btnDel);
+        }
+
+        JButton btnSave = new JButton("\u4fdd\u5b58\u5168\u90e8\u8bbe\u7f6e");
+        btnSave.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 20));
+        btnSave.setBounds(380, 1148, 180, 45);
         btnSave.addActionListener(e -> saveAllConfig());
         add(btnSave);
 
-        addLabel("每行表示「从起始颗到结束颗」使用同一套参数;开工时下发第1颗参数,每完成一颗后自动切换下一颗区间。",
-                20, 652, CONTENT_WIDTH, 40, hintFont);
+        addLabel("\u6bcf\u884c\u8868\u793a\u300c\u4ece\u8d77\u59cb\u9897\u5230\u7ed3\u675f\u9897\u300d\u4f7f\u7528\u540c\u4e00\u5957\u53c2\u6570\uff1b\u5f00\u5de5\u65f6\u4e0b\u53d1\u7b2c1\u9897\u53c2\u6570\uff0c\u6bcf\u5b8c\u6210\u4e00\u9897\u540e\u81ea\u52a8\u5207\u6362\u4e0b\u4e00\u9897\u533a\u95f4\u3002",
+                20, 1196, CONTENT_WIDTH, 40, hintFont);
     }
 
     private void addLabel(String text, int x, int y, int w, int h, Font font) {
@@ -122,7 +109,7 @@ public class DeviceManagePanel extends JPanel {
     private JTable createParamTable(DefaultTableModel model, int x, int y, int w, int h) {
         JTable table = new JTable(model);
         table.setRowHeight(28);
-        table.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        table.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 14));
         table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
         JScrollPane scrollPane = new JScrollPane(table);
         scrollPane.setBounds(x, y, w, h);
@@ -138,22 +125,24 @@ public class DeviceManagePanel extends JPanel {
     }
 
     private void loadConfigToUi() {
-        txtASet.setText(String.valueOf(MesClient.aSetNum));
-        txtBSet.setText(String.valueOf(MesClient.bSetNum));
-        txtIpA.setText(MesClient.curIpA);
-        txtIpB.setText(MesClient.curIpB);
-        fillTableModel(tableModelA, JdbcUtils.getRivetParams("A"));
-        fillTableModel(tableModelB, JdbcUtils.getRivetParams("B"));
+        for (int i = 0; i < GUN_TYPES.length; i++) {
+            txtSets[i].setText(String.valueOf(getSetNum(GUN_TYPES[i])));
+            txtIps[i].setText(getIp(GUN_TYPES[i]));
+            fillTableModel(tableModels[i], JdbcUtils.getRivetParams(GUN_TYPES[i]));
+        }
     }
 
-    // 重新从数据库加载到界面(进入设备管理页时调用)
     public void reloadFromDb() {
         Map<String, Object> config = JdbcUtils.getConfig();
         if (config.size() > 0) {
             MesClient.aSetNum = (Short) config.get("a_set_num");
             MesClient.bSetNum = (Short) config.get("b_set_num");
+            MesClient.cSetNum = (Short) config.get("c_set_num");
+            MesClient.dSetNum = (Short) config.get("d_set_num");
             MesClient.curIpA = (String) config.get("plc_ip_a");
             MesClient.curIpB = (String) config.get("plc_ip_b");
+            MesClient.curIpC = (String) config.get("plc_ip_c");
+            MesClient.curIpD = (String) config.get("plc_ip_d");
         }
         MesClient.loadRivetParamConfig();
         loadConfigToUi();
@@ -182,27 +171,26 @@ public class DeviceManagePanel extends JPanel {
                 continue;
             }
             if (startText.isEmpty() || endText.isEmpty()) {
-                throw new IllegalArgumentException(gunType + "枪第" + (i + 1) + "行起始颗/结束颗不能为空");
+                throw new IllegalArgumentException(gunType + "\u67aa\u7b2c" + (i + 1) + "\u884c\u8d77\u59cb\u9897/\u7ed3\u675f\u9897\u4e0d\u80fd\u4e3a\u7a7a");
             }
             int start = Integer.parseInt(startText);
             int end = Integer.parseInt(endText);
             if (start < 1 || end < start) {
-                throw new IllegalArgumentException(gunType + "枪第" + (i + 1) + "行颗数范围无效");
+                throw new IllegalArgumentException(gunType + "\u67aa\u7b2c" + (i + 1) + "\u884c\u9897\u6570\u8303\u56f4\u65e0\u6548");
             }
             int fMin = parseIntCell(model, i, 2);
             int fMax = parseIntCell(model, i, 3);
             int sMin = parseIntCell(model, i, 4);
             int sMax = parseIntCell(model, i, 5);
             if (fMin > fMax) {
-                throw new IllegalArgumentException(gunType + "枪第" + (i + 1) + "行 F-min 不能大于 F-max");
+                throw new IllegalArgumentException(gunType + "\u67aa\u7b2c" + (i + 1) + "\u884c F-min \u4e0d\u80fd\u5927\u4e8e F-max");
             }
             if (sMin > sMax) {
-                throw new IllegalArgumentException(gunType + "枪第" + (i + 1) + "行 S-min 不能大于 S-max");
+                throw new IllegalArgumentException(gunType + "\u67aa\u7b2c" + (i + 1) + "\u884c S-min \u4e0d\u80fd\u5927\u4e8e S-max");
             }
-            // PLC Int16 写入上限,避免溢出后下发错误值
             if (fMin < Short.MIN_VALUE || fMax > Short.MAX_VALUE
                     || sMin < Short.MIN_VALUE || sMax > Short.MAX_VALUE) {
-                throw new IllegalArgumentException(gunType + "枪第" + (i + 1) + "行参数超出范围(-32768~32767)");
+                throw new IllegalArgumentException(gunType + "\u67aa\u7b2c" + (i + 1) + "\u884c\u53c2\u6570\u8d85\u51fa\u8303\u56f4(-32768~32767)");
             }
             RivetParamRange range = new RivetParamRange();
             range.setGunType(gunType);
@@ -215,17 +203,16 @@ public class DeviceManagePanel extends JPanel {
             ranges.add(range);
         }
         if (ranges.isEmpty()) {
-            throw new IllegalArgumentException(gunType + "枪至少配置一行参数区间");
+            throw new IllegalArgumentException(gunType + "\u67aa\u81f3\u5c11\u914d\u7f6e\u4e00\u884c\u53c2\u6570\u533a\u95f4");
         }
-        // 检查区间是否重叠,重叠会导致下发时命中错误行
         for (int i = 0; i < ranges.size(); i++) {
             RivetParamRange a = ranges.get(i);
             for (int j = i + 1; j < ranges.size(); j++) {
                 RivetParamRange b = ranges.get(j);
                 if (a.getStartRivet() <= b.getEndRivet() && b.getStartRivet() <= a.getEndRivet()) {
-                    throw new IllegalArgumentException(gunType + "枪参数区间存在重叠:"
+                    throw new IllegalArgumentException(gunType + "\u67aa\u53c2\u6570\u533a\u95f4\u5b58\u5728\u91cd\u53e0\uff1a"
                             + a.getStartRivet() + "-" + a.getEndRivet()
-                            + "  " + b.getStartRivet() + "-" + b.getEndRivet());
+                            + " \u4e0e " + b.getStartRivet() + "-" + b.getEndRivet());
                 }
             }
         }
@@ -240,10 +227,9 @@ public class DeviceManagePanel extends JPanel {
         return Integer.parseInt(text);
     }
 
-    // 提交表格编辑并将参数下发到PLC
     private boolean pushRivetParamsToDevice(ModbusTcp plc, List<RivetParamRange> ranges, String gunType) {
         if (plc == null) {
-            System.out.println(gunType + "枪PLC未连接,跳过参数下发");
+            System.out.println(gunType + " gun PLC is not connected, skip parameter sync");
             return false;
         }
         try {
@@ -255,82 +241,131 @@ public class DeviceManagePanel extends JPanel {
         }
     }
 
+    @SuppressWarnings("unchecked")
     private void saveAllConfig() {
-        // 保存前先结束表格编辑,确保最新输入写入模型
-        if (tableA != null && tableA.isEditing()) {
-            tableA.getCellEditor().stopCellEditing();
-        }
-        if (tableB != null && tableB.isEditing()) {
-            tableB.getCellEditor().stopCellEditing();
+        for (JTable table : tables) {
+            if (table != null && table.isEditing()) {
+                table.getCellEditor().stopCellEditing();
+            }
         }
         try {
-            short newASet = Short.parseShort(txtASet.getText().trim());
-            short newBSet = Short.parseShort(txtBSet.getText().trim());
-            String newIpA = txtIpA.getText().trim();
-            String newIpB = txtIpB.getText().trim();
-            if (newIpA.isEmpty() || newIpB.isEmpty()) {
-                JOptionPane.showMessageDialog(MesClient.mesClientFrame, "IP地址不能为空", "错误", JOptionPane.ERROR_MESSAGE);
-                return;
+            short[] newSetNums = new short[GUN_TYPES.length];
+            String[] newIps = new String[GUN_TYPES.length];
+            List<RivetParamRange>[] ranges = new List[GUN_TYPES.length];
+
+            for (int i = 0; i < GUN_TYPES.length; i++) {
+                newSetNums[i] = Short.parseShort(txtSets[i].getText().trim());
+                newIps[i] = txtIps[i].getText().trim();
+                if (newIps[i].isEmpty()) {
+                    JOptionPane.showMessageDialog(MesClient.mesClientFrame, GUN_TYPES[i] + "\u67aaIP\u5730\u5740\u4e0d\u80fd\u4e3a\u7a7a", "\u9519\u8bef", JOptionPane.ERROR_MESSAGE);
+                    return;
+                }
+                ranges[i] = parseTableModel(tableModels[i], GUN_TYPES[i]);
             }
 
-            List<RivetParamRange> rangesA = parseTableModel(tableModelA, "A");
-            List<RivetParamRange> rangesB = parseTableModel(tableModelB, "B");
-
-            boolean configOk = JdbcUtils.updateConfig(newASet, newBSet, newIpA, newIpB);
-            boolean saveAOk = JdbcUtils.saveRivetParams("A", rangesA);
-            boolean saveBOk = JdbcUtils.saveRivetParams("B", rangesB);
-            if (!configOk || !saveAOk || !saveBOk) {
+            boolean configOk = JdbcUtils.updateConfig(newSetNums[0], newSetNums[1], newSetNums[2], newSetNums[3],
+                    newIps[0], newIps[1], newIps[2], newIps[3]);
+            boolean paramsOk = true;
+            for (int i = 0; i < GUN_TYPES.length; i++) {
+                paramsOk = JdbcUtils.saveRivetParams(GUN_TYPES[i], ranges[i]) && paramsOk;
+            }
+            if (!configOk || !paramsOk) {
                 JOptionPane.showMessageDialog(MesClient.mesClientFrame,
-                        "配置保存失败,请查看控制台日志后重试(常见原因:数据库被占用)",
-                        "错误", JOptionPane.ERROR_MESSAGE);
-                // 回读数据库,避免界面显示未真正落库的值
+                        "\u914d\u7f6e\u4fdd\u5b58\u5931\u8d25\uff0c\u8bf7\u67e5\u770b\u63a7\u5236\u53f0\u65e5\u5fd7\u540e\u91cd\u8bd5\uff08\u5e38\u89c1\u539f\u56e0\uff1a\u6570\u636e\u5e93\u88ab\u5360\u7528\uff09",
+                        "\u9519\u8bef", JOptionPane.ERROR_MESSAGE);
                 reloadFromDb();
                 return;
             }
 
-            MesClient.aSetNum = newASet;
-            MesClient.bSetNum = newBSet;
-            MesClient.rivetParamRangesA = rangesA;
-            MesClient.rivetParamRangesB = rangesB;
-
-            if (MesClient.param1 != null) {
-                MesClient.param1.setText(String.valueOf(MesClient.aSetNum));
-            }
-            if (MesClient.param3 != null) {
-                MesClient.param3.setText(String.valueOf(MesClient.bSetNum));
-            }
-
-            boolean ipAChanged = !newIpA.equals(MesClient.curIpA);
-            boolean ipBChanged = !newIpB.equals(MesClient.curIpB);
-            if (ipAChanged) {
-                if (MesClient.plcA != null) {
-                    MesClient.plcA.close();
-                }
-                MesClient.curIpA = newIpA;
-                MesClient.plcA = new ModbusTcp(1, MesClient.curIpA);
-            }
-            if (ipBChanged) {
-                if (MesClient.plcB != null) {
-                    MesClient.plcB.close();
-                }
-                MesClient.curIpB = newIpB;
-                MesClient.plcB = new ModbusTcp(1, MesClient.curIpB);
+            setSetNum("A", newSetNums[0]);
+            setSetNum("B", newSetNums[1]);
+            setSetNum("C", newSetNums[2]);
+            setSetNum("D", newSetNums[3]);
+            MesClient.rivetParamRangesA = ranges[0];
+            MesClient.rivetParamRangesB = ranges[1];
+            MesClient.rivetParamRangesC = ranges[2];
+            MesClient.rivetParamRangesD = ranges[3];
+
+            updateMainSetFields();
+
+            boolean syncOk = true;
+            for (int i = 0; i < GUN_TYPES.length; i++) {
+                syncOk = reconnectIfIpChanged(GUN_TYPES[i], newIps[i]) && syncOk;
+                syncOk = pushRivetParamsToDevice(getPlc(GUN_TYPES[i]), ranges[i], GUN_TYPES[i]) && syncOk;
             }
 
-            // 保存后立即将拉力/行程参数下发到设备
-            boolean syncA = pushRivetParamsToDevice(MesClient.plcA, rangesA, "A");
-            boolean syncB = pushRivetParamsToDevice(MesClient.plcB, rangesB, "B");
-            if (syncA && syncB) {
-                JOptionPane.showMessageDialog(MesClient.mesClientFrame, "配置已保存,并已下发到设备", "提示", JOptionPane.INFORMATION_MESSAGE);
+            if (syncOk) {
+                JOptionPane.showMessageDialog(MesClient.mesClientFrame, "\u914d\u7f6e\u5df2\u4fdd\u5b58\uff0c\u5e76\u5df2\u4e0b\u53d1\u5230\u8bbe\u5907", "\u63d0\u793a", JOptionPane.INFORMATION_MESSAGE);
             } else {
                 JOptionPane.showMessageDialog(MesClient.mesClientFrame,
-                        "配置已保存到本地,但部分设备参数下发失败,请检查设备连接;下次开工时会按新配置重新下发",
-                        "提示", JOptionPane.WARNING_MESSAGE);
+                        "\u914d\u7f6e\u5df2\u4fdd\u5b58\u5230\u672c\u5730\uff0c\u4f46\u90e8\u5206\u8bbe\u5907\u53c2\u6570\u4e0b\u53d1\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u8bbe\u5907\u8fde\u63a5\uff1b\u4e0b\u6b21\u5f00\u5de5\u65f6\u4f1a\u6309\u65b0\u914d\u7f6e\u91cd\u65b0\u4e0b\u53d1",
+                        "\u63d0\u793a", JOptionPane.WARNING_MESSAGE);
             }
         } catch (NumberFormatException ex) {
-            JOptionPane.showMessageDialog(MesClient.mesClientFrame, "请输入有效的数字", "错误", JOptionPane.ERROR_MESSAGE);
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame, "\u8bf7\u8f93\u5165\u6709\u6548\u7684\u6570\u5b57", "\u9519\u8bef", JOptionPane.ERROR_MESSAGE);
         } catch (IllegalArgumentException ex) {
-            JOptionPane.showMessageDialog(MesClient.mesClientFrame, ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame, ex.getMessage(), "\u9519\u8bef", JOptionPane.ERROR_MESSAGE);
         }
     }
+
+    private short getSetNum(String gunType) {
+        if ("A".equals(gunType)) return MesClient.aSetNum;
+        if ("B".equals(gunType)) return MesClient.bSetNum;
+        if ("C".equals(gunType)) return MesClient.cSetNum;
+        return MesClient.dSetNum;
+    }
+
+    private void setSetNum(String gunType, short value) {
+        if ("A".equals(gunType)) MesClient.aSetNum = value;
+        else if ("B".equals(gunType)) MesClient.bSetNum = value;
+        else if ("C".equals(gunType)) MesClient.cSetNum = value;
+        else MesClient.dSetNum = value;
+    }
+
+    private String getIp(String gunType) {
+        if ("A".equals(gunType)) return MesClient.curIpA;
+        if ("B".equals(gunType)) return MesClient.curIpB;
+        if ("C".equals(gunType)) return MesClient.curIpC;
+        return MesClient.curIpD;
+    }
+
+    private void setIp(String gunType, String value) {
+        if ("A".equals(gunType)) MesClient.curIpA = value;
+        else if ("B".equals(gunType)) MesClient.curIpB = value;
+        else if ("C".equals(gunType)) MesClient.curIpC = value;
+        else MesClient.curIpD = value;
+    }
+
+    private ModbusTcp getPlc(String gunType) {
+        if ("A".equals(gunType)) return MesClient.plcA;
+        if ("B".equals(gunType)) return MesClient.plcB;
+        if ("C".equals(gunType)) return MesClient.plcC;
+        return MesClient.plcD;
+    }
+
+    private void setPlc(String gunType, ModbusTcp plc) {
+        if ("A".equals(gunType)) MesClient.plcA = plc;
+        else if ("B".equals(gunType)) MesClient.plcB = plc;
+        else if ("C".equals(gunType)) MesClient.plcC = plc;
+        else MesClient.plcD = plc;
+    }
+
+    private boolean reconnectIfIpChanged(String gunType, String newIp) {
+        if (!newIp.equals(getIp(gunType))) {
+            ModbusTcp plc = getPlc(gunType);
+            if (plc != null) {
+                plc.close();
+            }
+            setIp(gunType, newIp);
+            setPlc(gunType, new ModbusTcp(1, newIp));
+        }
+        return true;
+    }
+
+    private void updateMainSetFields() {
+        if (MesClient.param1 != null) MesClient.param1.setText(String.valueOf(MesClient.aSetNum));
+        if (MesClient.param3 != null) MesClient.param3.setText(String.valueOf(MesClient.bSetNum));
+        if (MesClient.param9 != null) MesClient.param9.setText(String.valueOf(MesClient.cSetNum));
+        if (MesClient.param11 != null) MesClient.param11.setText(String.valueOf(MesClient.dSetNum));
+    }
 }

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

@@ -181,6 +181,8 @@ public class LoginFarme extends JFrame {
                     MesClient.welcomeWin.setVisible(false);
                     MesClient.mesClientFrame.setVisible(true);
                     MesClient.startGetCurSn();
+                    // 上报设备运行状态并启动定时上报
+                    MesClient.reportDeviceRunning();
 
                     if(MesClient.jfxPanel == null){
                         String url = "http://"+ MesClient.mes_server_ip+":8980/js/a/mes/mesProductRecord/work?__sid="+urlEncode(MesClient.sessionid)+"&oprno="+urlEncode(MesClient.mes_gw)+"&lineSn="+urlEncode(MesClient.mes_line_sn);

+ 323 - 24
src/com/mes/ui/MesClient.java

@@ -5,7 +5,9 @@ import com.alibaba.fastjson2.JSONObject;
 import com.github.xingshuangs.iot.protocol.modbus.service.ModbusTcp;
 import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
+import com.mes.device.DeviceStateReporter;
 import com.mes.netty.NettyClient;
+import com.mes.update.ClientAutoUpdater;
 import com.mes.util.DateLocalUtils;
 import com.mes.util.JdbcUtils;
 import javafx.embed.swing.JFXPanel;
@@ -41,6 +43,8 @@ public class MesClient extends JFrame {
     public static String mes_line_sn = ""; // 产线编号
 
     public static boolean mes_shield_flag = false; // MES 屏蔽标志
+    public static int mes_device_heartbeat_minutes = 5; // 设备状态上报间隔(分钟)
+
 
     //TCP连接
     public static NettyClient nettyClient;
@@ -88,11 +92,23 @@ public class MesClient extends JFrame {
     public static JTextField param6;
     public static JTextField param7;
     public static JTextField param8;
+    public static JTextField param9;
+    public static JTextField param10;
+    public static JTextField param11;
+    public static JTextField param12;
+    public static JTextField param13;
+    public static JTextField param14;
+    public static JTextField param15;
+    public static JTextField param16;
 
     public static ModbusTcp plcA;
     public static ModbusTcp plcB;
+    public static ModbusTcp plcC;
+    public static ModbusTcp plcD;
     public static String curIpA = "";
     public static String curIpB = "";
+    public static String curIpC = "";
+    public static String curIpD = "";
 //    public static ModbusTcp plcA = new ModbusTcp(1, "192.168.1.27");
 //    public static ModbusTcp plcB = new ModbusTcp(1, "192.168.1.28");
 
@@ -112,7 +128,18 @@ public class MesClient extends JFrame {
     //    public static Short bSetNum = 27;
     public static Short sortB = 0;
     public static Short bMax = 0;
+    public static Short bFinish = 0;
     public static List<Map> blist = new ArrayList<>();
+    public static Short cSetNum = 14;
+    public static Short sortC = 0;
+    public static Short cMax = 0;
+    public static Short cFinish = 0;
+    public static List<Map> clist = new ArrayList<>();
+    public static Short dSetNum = 14;
+    public static Short sortD = 0;
+    public static Short dMax = 0;
+    public static Short dFinish = 0;
+    public static List<Map> dlist = new ArrayList<>();
     public static Short deviceControl = 0; // 0=本地 1=远程
     public static Integer tjStatus = 0; // 1=提交失败
     public static JPanel indexPanelB;
@@ -122,6 +149,8 @@ public class MesClient extends JFrame {
     // 铆接参数区间配置(内存缓存)
     public static List<RivetParamRange> rivetParamRangesA = new ArrayList<>();
     public static List<RivetParamRange> rivetParamRangesB = new ArrayList<>();
+    public static List<RivetParamRange> rivetParamRangesC = new ArrayList<>();
+    public static List<RivetParamRange> rivetParamRangesD = new ArrayList<>();
 
     public static String tjFlagTextErr = "结果上传MES失败,请重试";
 
@@ -143,8 +172,11 @@ public class MesClient extends JFrame {
             @Override
             public void run() {
                 try{
-                    // 读文件配置
-                    readProperty();
+                    Properties config = readProperty();
+                    // 程序启动时先检查是否有当前工位的新 jar;如需升级,会退出并交给外部 bat 替换 jar。
+                    if (ClientAutoUpdater.checkAndUpdate(config)) {
+                        return;
+                    }
 
                     JdbcUtils.getConn();
 
@@ -186,6 +218,8 @@ public class MesClient extends JFrame {
                         if (work_status == 1) {
                             keepGunRunningIfNeeded(plcA, sortA, aMax);
                             keepGunRunningIfNeeded(plcB, sortB, bMax);
+                            keepGunRunningIfNeeded(plcC, sortC, cMax);
+                            keepGunRunningIfNeeded(plcD, sortD, dMax);
                         }
                         return;
                     }
@@ -207,6 +241,24 @@ public class MesClient extends JFrame {
                                 ModbusUtil.setPowerOn(plcB);
                             }
                         }
+
+                        List<Boolean> yxstatusc = plcC.readCoil(3128,1);
+                        if(yxstatusc.size() >= 1 && !yxstatusc.get(0)){
+                            if(!MesClient.curSn.isEmpty() && MesClient.cMax > 0 && MesClient.cMax == MesClient.sortC){
+                                ModbusUtil.setPowerOff(plcC);
+                            }else{
+                                ModbusUtil.setPowerOn(plcC);
+                            }
+                        }
+
+                        List<Boolean> yxstatusd = plcD.readCoil(3128,1);
+                        if(yxstatusd.size() >= 1 && !yxstatusd.get(0)){
+                            if(!MesClient.curSn.isEmpty() && MesClient.dMax > 0 && MesClient.dMax == MesClient.sortD){
+                                ModbusUtil.setPowerOff(plcD);
+                            }else{
+                                ModbusUtil.setPowerOn(plcD);
+                            }
+                        }
                     }else{
                         List<Boolean> yxstatus = plcA.readCoil(3128,1);
                         if(yxstatus.size() >= 1 && yxstatus.get(0)){
@@ -217,6 +269,16 @@ public class MesClient extends JFrame {
                         if(yxstatusb.size() >= 1 && yxstatusb.get(0)){
                             ModbusUtil.setPowerOff(plcB);
                         }
+
+                        List<Boolean> yxstatusc = plcC.readCoil(3128,1);
+                        if(yxstatusc.size() >= 1 && yxstatusc.get(0)){
+                            ModbusUtil.setPowerOff(plcC);
+                        }
+
+                        List<Boolean> yxstatusd = plcD.readCoil(3128,1);
+                        if(yxstatusd.size() >= 1 && yxstatusd.get(0)){
+                            ModbusUtil.setPowerOff(plcD);
+                        }
                     }
                 }catch (Exception e){
                     e.printStackTrace();
@@ -227,6 +289,9 @@ public class MesClient extends JFrame {
 
     // MES屏蔽期间:任务未完成时保持拉铆枪运行
     private static void keepGunRunningIfNeeded(ModbusTcp plc, Short sort, Short max) {
+        if (plc == null) {
+            return;
+        }
         try {
             List<Boolean> yxstatus = plc.readCoil(3128, 1);
             if (yxstatus.size() >= 1 && !yxstatus.get(0)) {
@@ -240,7 +305,7 @@ public class MesClient extends JFrame {
     }
 
     //读配置文件
-    private static void readProperty() throws IOException{
+    private static Properties readProperty() throws IOException{
         String enconding = "UTF-8";
         InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
         Properties pro = new Properties();
@@ -255,8 +320,16 @@ public class MesClient extends JFrame {
         mes_line_sn = pro.getProperty("mes.line_sn");
 
         mes_gw_des = OprnoUtil.getGwDes(mes_line_sn,mes_gw);
-
+        try {
+            mes_device_heartbeat_minutes = Integer.parseInt(pro.getProperty("mes.device.heartbeat.minutes", "5").trim());
+        } catch (Exception e) {
+            mes_device_heartbeat_minutes = 5;
+        }
+        if (mes_device_heartbeat_minutes <= 0) {
+            mes_device_heartbeat_minutes = 5;
+        }
         System.out.println(mes_gw + ";" + mes_gw_des + ";" + mes_server_ip + ";" + mes_tcp_port + ";" + mes_heart_beat_cycle);
+        return pro;
     }
 
     private static void initLocalConfig() {
@@ -264,24 +337,38 @@ public class MesClient extends JFrame {
         if (config.size() > 0) {
             aSetNum = (Short) config.get("a_set_num");
             bSetNum = (Short) config.get("b_set_num");
+            cSetNum = (Short) config.get("c_set_num");
+            dSetNum = (Short) config.get("d_set_num");
             curIpA = (String) config.get("plc_ip_a");
             curIpB = (String) config.get("plc_ip_b");
+            curIpC = (String) config.get("plc_ip_c");
+            curIpD = (String) config.get("plc_ip_d");
             plcA = new ModbusTcp(1, curIpA);
             plcB = new ModbusTcp(1, curIpB);
+            plcC = new ModbusTcp(1, curIpC);
+            plcD = new ModbusTcp(1, curIpD);
             
             // 初始化工作面板显示的预设数量
             if (param1 != null) param1.setText(String.valueOf(aSetNum));
             if (param3 != null) param3.setText(String.valueOf(bSetNum));
+            if (param9 != null) param9.setText(String.valueOf(cSetNum));
+            if (param11 != null) param11.setText(String.valueOf(dSetNum));
             
             System.out.println("加载本地配置成功:aSetNum=" + aSetNum + ", bSetNum=" + bSetNum + ", plcA=" + curIpA + ", plcB=" + curIpB);
         } else {
             // 兜底默认值
             aSetNum = 41;
             bSetNum = 14;
+            cSetNum = 14;
+            dSetNum = 14;
             curIpA = "192.168.1.7";
             curIpB = "192.168.1.8";
+            curIpC = "192.168.1.9";
+            curIpD = "192.168.1.10";
             plcA = new ModbusTcp(1, curIpA);
             plcB = new ModbusTcp(1, curIpB);
+            plcC = new ModbusTcp(1, curIpC);
+            plcD = new ModbusTcp(1, curIpD);
             System.out.println("未找到本地配置,使用默认值");
         }
         loadRivetParamConfig();
@@ -291,6 +378,8 @@ public class MesClient extends JFrame {
     public static void loadRivetParamConfig() {
         rivetParamRangesA = JdbcUtils.getRivetParams("A");
         rivetParamRangesB = JdbcUtils.getRivetParams("B");
+        rivetParamRangesC = JdbcUtils.getRivetParams("C");
+        rivetParamRangesD = JdbcUtils.getRivetParams("D");
     }
 
     public static void getPlcParam() {
@@ -317,6 +406,24 @@ public class MesClient extends JFrame {
                 }catch (Exception e){
                     e.printStackTrace();
                 }
+
+                try{
+                    if(work_status == 1 && !mes_shield_flag){
+                        ModbusUtil.getDataC(plcC);
+                    }
+
+                }catch (Exception e){
+                    e.printStackTrace();
+                }
+
+                try{
+                    if(work_status == 1 && !mes_shield_flag){
+                        ModbusUtil.getDataD(plcD);
+                    }
+
+                }catch (Exception e){
+                    e.printStackTrace();
+                }
             }
         }, 1000,500);
     }
@@ -602,6 +709,8 @@ public class MesClient extends JFrame {
 
         ModbusUtil.setPowerOff(plcA);
         ModbusUtil.setPowerOff(plcB);
+        ModbusUtil.setPowerOff(plcC);
+        ModbusUtil.setPowerOff(plcD);
         return false;
     }
 
@@ -655,15 +764,28 @@ public class MesClient extends JFrame {
 
         MesClient.aMax = 0;
         MesClient.bMax = 0;
+        MesClient.cMax = 0;
+        MesClient.dMax = 0;
         MesClient.aFinish = 0;
+        MesClient.bFinish = 0;
+        MesClient.cFinish = 0;
+        MesClient.dFinish = 0;
         MesClient.alist = new ArrayList<>();
         MesClient.blist = new ArrayList<>();
+        MesClient.clist = new ArrayList<>();
+        MesClient.dlist = new ArrayList<>();
         MesClient.sortB = 0;
         MesClient.sortA = 0;
+        MesClient.sortC = 0;
+        MesClient.sortD = 0;
         MesClient.param1.setText(String.valueOf(aSetNum));
         MesClient.param2.setText("");
         MesClient.param3.setText(String.valueOf(bSetNum));
         MesClient.param4.setText("");
+        MesClient.param9.setText(String.valueOf(cSetNum));
+        MesClient.param10.setText("");
+        MesClient.param11.setText(String.valueOf(dSetNum));
+        MesClient.param12.setText("");
         resetForceStrokeDisplay();
 
         deviceControl = ModbusUtil.getControlModel(plcA);
@@ -676,8 +798,13 @@ public class MesClient extends JFrame {
         ModbusUtil.setPowerOff(MesClient.plcA); // 远程关机
         ModbusUtil.setPowerOff(MesClient.plcB); // 远程关机
 
+        ModbusUtil.setPowerOff(MesClient.plcC);
+        ModbusUtil.setPowerOff(MesClient.plcD);
+
         ModbusUtil.setTask(MesClient.plcA, MesClient.aSetNum, "A");
         ModbusUtil.setTask(MesClient.plcB, MesClient.bSetNum, "B");
+        ModbusUtil.setTask(MesClient.plcC, MesClient.cSetNum, "C");
+        ModbusUtil.setTask(MesClient.plcD, MesClient.dSetNum, "D");
 
         updateMaterailData();
     }
@@ -721,6 +848,10 @@ public class MesClient extends JFrame {
         if (param6 != null) param6.setText("-");
         if (param7 != null) param7.setText("-");
         if (param8 != null) param8.setText("-");
+        if (param13 != null) param13.setText("-");
+        if (param14 != null) param14.setText("-");
+        if (param15 != null) param15.setText("-");
+        if (param16 != null) param16.setText("-");
     }
 
     public static void logoff() {
@@ -904,6 +1035,7 @@ public class MesClient extends JFrame {
         JPanel indexPanelA = new JPanel();
         indexScrollPaneA = new JScrollPane(indexPanelA);
         indexPanelA.setLayout(null);
+        indexPanelA.setPreferredSize(new Dimension(960, 720));
 
         product_sn = new JTextField();
         product_sn.setHorizontalAlignment(SwingConstants.CENTER);
@@ -930,7 +1062,7 @@ public class MesClient extends JFrame {
         finish_ok_bt.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
         finish_ok_bt.setFont(new Font("微软雅黑", Font.PLAIN, 32));
 //        finish_ok_bt.setBounds(185, 291, 240, 80);
-        finish_ok_bt.setBounds(185, 371, 240, 80);
+        finish_ok_bt.setBounds(185, 430, 240, 80);
         finish_ok_bt.setEnabled(false);
         indexPanelA.add(finish_ok_bt);
 
@@ -943,19 +1075,19 @@ public class MesClient extends JFrame {
         });
         finish_ng_bt.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ng_bg.png")));
         finish_ng_bt.setFont(new Font("微软雅黑", Font.PLAIN, 32));
-        finish_ng_bt.setBounds(508, 371, 240, 80);
+        finish_ng_bt.setBounds(508, 430, 240, 80);
         finish_ng_bt.setEnabled(false);
         indexPanelA.add(finish_ng_bt);
 
         JLabel lblNewLabel = new JLabel("A\u67AA");
         lblNewLabel.setHorizontalAlignment(SwingConstants.CENTER);
         lblNewLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        lblNewLabel.setBounds(204, 186, 166, 28);
+        lblNewLabel.setBounds(80, 186, 190, 28);
         indexPanelA.add(lblNewLabel);
 
         JLabel lblNewLabel_1 = new JLabel("\u9884\u8BBE\u6570\u91CF");
         lblNewLabel_1.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblNewLabel_1.setBounds(204, 241, 83, 34);
+        lblNewLabel_1.setBounds(80, 241, 83, 34);
         indexPanelA.add(lblNewLabel_1);
 
         param1 = new JTextField();
@@ -963,13 +1095,13 @@ public class MesClient extends JFrame {
         param1.setFont(new Font("微软雅黑", Font.PLAIN, 18));
         param1.setText(String.valueOf(aSetNum));
         param1.setEditable(false);
-        param1.setBounds(288, 241, 83, 34);
+        param1.setBounds(164, 241, 65, 34);
         indexPanelA.add(param1);
         param1.setColumns(10);
 
         JLabel lblNewLabel_1_1 = new JLabel("\u5B8C\u6210\u6570\u91CF");
         lblNewLabel_1_1.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblNewLabel_1_1.setBounds(204, 285, 83, 34);
+        lblNewLabel_1_1.setBounds(80, 285, 83, 34);
         indexPanelA.add(lblNewLabel_1_1);
 
         param2 = new JTextField();
@@ -978,18 +1110,18 @@ public class MesClient extends JFrame {
         param2.setText("0");
         param2.setEditable(false);
         param2.setColumns(10);
-        param2.setBounds(288, 285, 83, 34);
+        param2.setBounds(164, 285, 65, 34);
         indexPanelA.add(param2);
 
         JLabel lblB = new JLabel("B\u67AA");
         lblB.setHorizontalAlignment(SwingConstants.CENTER);
         lblB.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        lblB.setBounds(525, 186, 166, 28);
+        lblB.setBounds(295, 186, 190, 28);
         indexPanelA.add(lblB);
 
         JLabel lblNewLabel_1_2 = new JLabel("\u9884\u8BBE\u6570\u91CF");
         lblNewLabel_1_2.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblNewLabel_1_2.setBounds(525, 241, 83, 34);
+        lblNewLabel_1_2.setBounds(295, 241, 83, 34);
         indexPanelA.add(lblNewLabel_1_2);
 
         param3 = new JTextField();
@@ -998,12 +1130,12 @@ public class MesClient extends JFrame {
         param3.setFont(new Font("微软雅黑", Font.PLAIN, 18));
         param3.setEditable(false);
         param3.setColumns(10);
-        param3.setBounds(608, 241, 83, 34);
+        param3.setBounds(379, 241, 65, 34);
         indexPanelA.add(param3);
 
         JLabel lblNewLabel_1_1_1 = new JLabel("\u5B8C\u6210\u6570\u91CF");
         lblNewLabel_1_1_1.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblNewLabel_1_1_1.setBounds(525, 285, 83, 34);
+        lblNewLabel_1_1_1.setBounds(295, 285, 83, 34);
         indexPanelA.add(lblNewLabel_1_1_1);
 
         param4 = new JTextField();
@@ -1012,12 +1144,12 @@ public class MesClient extends JFrame {
         param4.setFont(new Font("微软雅黑", Font.PLAIN, 18));
         param4.setEditable(false);
         param4.setColumns(10);
-        param4.setBounds(608, 285, 83, 34);
+        param4.setBounds(379, 285, 65, 34);
         indexPanelA.add(param4);
 
         JLabel lblForceA = new JLabel("\u62c9\u529b");
         lblForceA.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblForceA.setBounds(204, 329, 50, 34);
+        lblForceA.setBounds(80, 329, 45, 34);
         indexPanelA.add(lblForceA);
 
         param5 = new JTextField();
@@ -1026,12 +1158,12 @@ public class MesClient extends JFrame {
         param5.setText("-");
         param5.setEditable(false);
         param5.setColumns(10);
-        param5.setBounds(260, 329, 65, 34);
+        param5.setBounds(125, 329, 55, 34);
         indexPanelA.add(param5);
 
         JLabel lblStrokeA = new JLabel("\u884c\u7a0b");
         lblStrokeA.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblStrokeA.setBounds(330, 329, 50, 34);
+        lblStrokeA.setBounds(185, 329, 45, 34);
         indexPanelA.add(lblStrokeA);
 
         param6 = new JTextField();
@@ -1040,12 +1172,12 @@ public class MesClient extends JFrame {
         param6.setText("-");
         param6.setEditable(false);
         param6.setColumns(10);
-        param6.setBounds(380, 329, 65, 34);
+        param6.setBounds(230, 329, 55, 34);
         indexPanelA.add(param6);
 
         JLabel lblForceB = new JLabel("\u62c9\u529b");
         lblForceB.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblForceB.setBounds(525, 329, 50, 34);
+        lblForceB.setBounds(295, 329, 45, 34);
         indexPanelA.add(lblForceB);
 
         param7 = new JTextField();
@@ -1054,12 +1186,12 @@ public class MesClient extends JFrame {
         param7.setText("-");
         param7.setEditable(false);
         param7.setColumns(10);
-        param7.setBounds(581, 329, 65, 34);
+        param7.setBounds(340, 329, 55, 34);
         indexPanelA.add(param7);
 
         JLabel lblStrokeB = new JLabel("\u884c\u7a0b");
         lblStrokeB.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        lblStrokeB.setBounds(651, 329, 50, 34);
+        lblStrokeB.setBounds(400, 329, 45, 34);
         indexPanelA.add(lblStrokeB);
 
         param8 = new JTextField();
@@ -1068,9 +1200,133 @@ public class MesClient extends JFrame {
         param8.setText("-");
         param8.setEditable(false);
         param8.setColumns(10);
-        param8.setBounds(701, 329, 65, 34);
+        param8.setBounds(445, 329, 55, 34);
         indexPanelA.add(param8);
 
+        JLabel lblC = new JLabel("C\u67AA");
+        lblC.setHorizontalAlignment(SwingConstants.CENTER);
+        lblC.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 20));
+        lblC.setBounds(510, 186, 190, 28);
+        indexPanelA.add(lblC);
+
+        JLabel lblCSet = new JLabel("\u9884\u8BBE\u6570\u91CF");
+        lblCSet.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblCSet.setBounds(510, 241, 83, 34);
+        indexPanelA.add(lblCSet);
+
+        param9 = new JTextField();
+        param9.setHorizontalAlignment(SwingConstants.CENTER);
+        param9.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param9.setText(String.valueOf(cSetNum));
+        param9.setEditable(false);
+        param9.setBounds(594, 241, 65, 34);
+        indexPanelA.add(param9);
+        param9.setColumns(10);
+
+        JLabel lblCDone = new JLabel("\u5B8C\u6210\u6570\u91CF");
+        lblCDone.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblCDone.setBounds(510, 285, 83, 34);
+        indexPanelA.add(lblCDone);
+
+        param10 = new JTextField();
+        param10.setHorizontalAlignment(SwingConstants.CENTER);
+        param10.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param10.setText("0");
+        param10.setEditable(false);
+        param10.setColumns(10);
+        param10.setBounds(594, 285, 65, 34);
+        indexPanelA.add(param10);
+
+        JLabel lblD = new JLabel("D\u67AA");
+        lblD.setHorizontalAlignment(SwingConstants.CENTER);
+        lblD.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 20));
+        lblD.setBounds(725, 186, 190, 28);
+        indexPanelA.add(lblD);
+
+        JLabel lblDSet = new JLabel("\u9884\u8BBE\u6570\u91CF");
+        lblDSet.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblDSet.setBounds(725, 241, 83, 34);
+        indexPanelA.add(lblDSet);
+
+        param11 = new JTextField();
+        param11.setText(String.valueOf(dSetNum));
+        param11.setHorizontalAlignment(SwingConstants.CENTER);
+        param11.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param11.setEditable(false);
+        param11.setColumns(10);
+        param11.setBounds(809, 241, 65, 34);
+        indexPanelA.add(param11);
+
+        JLabel lblDDone = new JLabel("\u5B8C\u6210\u6570\u91CF");
+        lblDDone.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblDDone.setBounds(725, 285, 83, 34);
+        indexPanelA.add(lblDDone);
+
+        param12 = new JTextField();
+        param12.setText("0");
+        param12.setHorizontalAlignment(SwingConstants.CENTER);
+        param12.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param12.setEditable(false);
+        param12.setColumns(10);
+        param12.setBounds(809, 285, 65, 34);
+        indexPanelA.add(param12);
+
+        JLabel lblForceC = new JLabel("\u62c9\u529b");
+        lblForceC.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblForceC.setBounds(510, 329, 45, 34);
+        indexPanelA.add(lblForceC);
+
+        param13 = new JTextField();
+        param13.setHorizontalAlignment(SwingConstants.CENTER);
+        param13.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param13.setText("-");
+        param13.setEditable(false);
+        param13.setColumns(10);
+        param13.setBounds(555, 329, 55, 34);
+        indexPanelA.add(param13);
+
+        JLabel lblStrokeC = new JLabel("\u884c\u7a0b");
+        lblStrokeC.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblStrokeC.setBounds(615, 329, 45, 34);
+        indexPanelA.add(lblStrokeC);
+
+        param14 = new JTextField();
+        param14.setHorizontalAlignment(SwingConstants.CENTER);
+        param14.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param14.setText("-");
+        param14.setEditable(false);
+        param14.setColumns(10);
+        param14.setBounds(660, 329, 55, 34);
+        indexPanelA.add(param14);
+
+        JLabel lblForceD = new JLabel("\u62c9\u529b");
+        lblForceD.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblForceD.setBounds(725, 329, 45, 34);
+        indexPanelA.add(lblForceD);
+
+        param15 = new JTextField();
+        param15.setHorizontalAlignment(SwingConstants.CENTER);
+        param15.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param15.setText("-");
+        param15.setEditable(false);
+        param15.setColumns(10);
+        param15.setBounds(770, 329, 55, 34);
+        indexPanelA.add(param15);
+
+        JLabel lblStrokeD = new JLabel("\u884c\u7a0b");
+        lblStrokeD.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        lblStrokeD.setBounds(830, 329, 45, 34);
+        indexPanelA.add(lblStrokeD);
+
+        param16 = new JTextField();
+        param16.setHorizontalAlignment(SwingConstants.CENTER);
+        param16.setFont(new Font("\u5fae\u8f6f\u96c5\u9ed1", Font.PLAIN, 18));
+        param16.setText("-");
+        param16.setEditable(false);
+        param16.setColumns(10);
+        param16.setBounds(875, 329, 55, 34);
+        indexPanelA.add(param16);
+
         tabbedPane.addTab("工作面板", new ImageIcon(MesClient.class.getResource("/bg/a_side.png")), indexScrollPaneA, null);
         tabbedPane.setEnabledAt(0, true);
 
@@ -1227,5 +1483,48 @@ public class MesClient extends JFrame {
         indexPanelBB.setLayout(null);
         tabbedPane.addTab("绑定物料", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), indexScrollPaneB, null);
     }
+    // 设备状态定时上报
+    public static java.util.Timer deviceReportTimer;
+    /**
+     * 启动设备状态定时上报
+     */
+    public static void startDeviceReportTimer() {
+        stopDeviceReportTimer();
+        long intervalMs = mes_device_heartbeat_minutes * 60L * 1000L;
+        deviceReportTimer = new Timer("device-report-timer", true);
+        deviceReportTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                DeviceStateReporter.getInstance().reportAsync(DeviceStateReporter.STATE_RUNNING, "运行心跳");
+            }
+        }, intervalMs, intervalMs);
+    }
+
+    /**
+     * 停止设备状态定时上报
+     */
+    public static void stopDeviceReportTimer() {
+        if (deviceReportTimer != null) {
+            deviceReportTimer.cancel();
+            deviceReportTimer = null;
+        }
+    }
+
+    /**
+     * 登录成功后上报设备运行状态
+     */
+    public static void reportDeviceRunning() {
+        DeviceStateReporter.getInstance().init(mes_server_ip, mes_gw, mes_line_sn);
+        DeviceStateReporter.getInstance().reportAsync(DeviceStateReporter.STATE_RUNNING, "工位登录,开始运行");
+        startDeviceReportTimer();
+    }
+
+    /**
+     * 退出时停止上报并发送停止状态
+     */
+    public static void reportDeviceStopped() {
+        stopDeviceReportTimer();
+        DeviceStateReporter.getInstance().reportSync(DeviceStateReporter.STATE_STOP, "工位退出,停止运行");
+    }
 
 }

+ 116 - 7
src/com/mes/ui/ModbusUtil.java

@@ -11,7 +11,7 @@ public class ModbusUtil {
 
     // 刷新界面拉力、行程显示
     private static void updateForceStrokeDisplay(ModbusTcp plc, JTextField forceField, JTextField strokeField) {
-        if (forceField == null || strokeField == null) {
+        if (plc == null || forceField == null || strokeField == null) {
             return;
         }
         try {
@@ -100,6 +100,7 @@ public class ModbusUtil {
 
         if(MesClient.sortB == cur - 1){
             MesClient.sortB = cur;
+            MesClient.bFinish = plc.readInt16(1138);
             String fout = plc.readInt16(1064)+"";
             String sout = (float)plc.readInt16(1065)/1000+"";
 
@@ -124,16 +125,99 @@ public class ModbusUtil {
 
     }
 
+    public static void getDataC(ModbusTcp plc){
+
+        Short cur = plc.readInt16(1136);
+        updateForceStrokeDisplay(plc, MesClient.param13, MesClient.param14);
+
+        if(MesClient.cMax <= 0){
+            MesClient.cMax = plc.readInt16(1128);
+            MesClient.param9.setText(String.valueOf(MesClient.cMax));
+        }
+        MesClient.param10.setText(cur + "");
+
+        if(MesClient.sortC == cur - 1){
+            MesClient.sortC = cur;
+            MesClient.cFinish = plc.readInt16(1138);
+            String fout = plc.readInt16(1064)+"";
+            String sout = (float)plc.readInt16(1065)/1000+"";
+
+            String fmin = plc.readInt16(1068)+"";
+            String smin = (float)plc.readInt16(1070)/1000+"";
+
+            String fmax = plc.readInt16(1069)+"";
+            String smax = (float)plc.readInt16(1071)/1000+"";
+
+            int nextRivet = cur + 1;
+            if (nextRivet <= MesClient.cMax) {
+                applyParamsForRivet(plc, MesClient.rivetParamRangesC, nextRivet);
+            }
+
+            System.out.println("cur:"+cur);
+            if(!MesClient.product_sn.getText().isEmpty()){
+                JdbcUtils.insertProdData(MesClient.mes_gw, MesClient.mes_line_sn, MesClient.product_sn.getText(),"C",fout,sout,fmin,smin,fmax,smax,"1",cur+"", MesClient.user_menu.getText());
+            }
+        }
+
+        upResult();
+
+    }
+
+    public static void getDataD(ModbusTcp plc){
+
+        Short cur = plc.readInt16(1136);
+        updateForceStrokeDisplay(plc, MesClient.param15, MesClient.param16);
+
+        if(MesClient.dMax <= 0){
+            MesClient.dMax = plc.readInt16(1128);
+            MesClient.param11.setText(String.valueOf(MesClient.dMax));
+        }
+        MesClient.param12.setText(cur + "");
+
+        if(MesClient.sortD == cur - 1){
+            MesClient.sortD = cur;
+            MesClient.dFinish = plc.readInt16(1138);
+            String fout = plc.readInt16(1064)+"";
+            String sout = (float)plc.readInt16(1065)/1000+"";
+
+            String fmin = plc.readInt16(1068)+"";
+            String smin = (float)plc.readInt16(1070)/1000+"";
+
+            String fmax = plc.readInt16(1069)+"";
+            String smax = (float)plc.readInt16(1071)/1000+"";
+
+            int nextRivet = cur + 1;
+            if (nextRivet <= MesClient.dMax) {
+                applyParamsForRivet(plc, MesClient.rivetParamRangesD, nextRivet);
+            }
+
+            System.out.println("cur:"+cur);
+            if(!MesClient.product_sn.getText().isEmpty()){
+                JdbcUtils.insertProdData(MesClient.mes_gw, MesClient.mes_line_sn, MesClient.product_sn.getText(),"D",fout,sout,fmin,smin,fmax,smax,"1",cur+"", MesClient.user_menu.getText());
+            }
+        }
+
+        upResult();
+
+    }
+
     // 上传总结果
     public static void upResult(){
-        if(!MesClient.curSn.isEmpty() && MesClient.aMax > 0 && MesClient.aMax == MesClient.sortA && MesClient.bMax > 0 && MesClient.bMax == MesClient.sortB){
+        if(!MesClient.curSn.isEmpty()
+                && MesClient.aMax > 0 && MesClient.aMax == MesClient.sortA
+                && MesClient.bMax > 0 && MesClient.bMax == MesClient.sortB
+                && MesClient.cMax > 0 && MesClient.cMax == MesClient.sortC
+                && MesClient.dMax > 0 && MesClient.dMax == MesClient.sortD){
             MesClient.finish_ok_bt.setEnabled(true);
             MesClient.finish_ng_bt.setEnabled(true);
 
             if(MesClient.tjStatus == 0 && MesClient.check_quality_result){
                 MesClient.tjStatus = 1;
                 String retf = "OK";
-                if(MesClient.aFinish != MesClient.aMax){
+                if(MesClient.aFinish != MesClient.aMax
+                        || MesClient.bFinish != MesClient.bMax
+                        || MesClient.cFinish != MesClient.cMax
+                        || MesClient.dFinish != MesClient.dMax){
                     retf = "NG";
                 }
                 MesClient.submitQualityResult(retf);
@@ -189,6 +273,9 @@ public class ModbusUtil {
 
     // 远程开机
     public static Boolean setPowerOn(ModbusTcp plc){
+        if (plc == null) {
+            return false;
+        }
         Boolean ret = false;
         try{
             plc.writeCoil(3128,true);
@@ -205,6 +292,9 @@ public class ModbusUtil {
 
     // 远程关机
     public static Boolean setPowerOff(ModbusTcp plc){
+        if (plc == null) {
+            return false;
+        }
         if (MesClient.mes_shield_flag) {
             System.out.println("MES屏蔽已开启,跳过关机指令");
             return true;
@@ -239,12 +329,24 @@ public class ModbusUtil {
 
     // 获取指定枪的参数区间,必要时从数据库重新加载
     private static List<RivetParamRange> getRivetParamRanges(String gunType) {
-        List<RivetParamRange> ranges = "A".equalsIgnoreCase(gunType)
-                ? MesClient.rivetParamRangesA : MesClient.rivetParamRangesB;
+        List<RivetParamRange> ranges = MesClient.rivetParamRangesD;
+        if ("A".equalsIgnoreCase(gunType)) {
+            ranges = MesClient.rivetParamRangesA;
+        } else if ("B".equalsIgnoreCase(gunType)) {
+            ranges = MesClient.rivetParamRangesB;
+        } else if ("C".equalsIgnoreCase(gunType)) {
+            ranges = MesClient.rivetParamRangesC;
+        }
         if (ranges == null || ranges.isEmpty()) {
             MesClient.loadRivetParamConfig();
-            ranges = "A".equalsIgnoreCase(gunType)
-                    ? MesClient.rivetParamRangesA : MesClient.rivetParamRangesB;
+            ranges = MesClient.rivetParamRangesD;
+            if ("A".equalsIgnoreCase(gunType)) {
+                ranges = MesClient.rivetParamRangesA;
+            } else if ("B".equalsIgnoreCase(gunType)) {
+                ranges = MesClient.rivetParamRangesB;
+            } else if ("C".equalsIgnoreCase(gunType)) {
+                ranges = MesClient.rivetParamRangesC;
+            }
         }
         return ranges;
     }
@@ -254,8 +356,12 @@ public class ModbusUtil {
         MesClient.loadRivetParamConfig();
         setPowerOn(MesClient.plcA);
         setPowerOn(MesClient.plcB);
+        setPowerOn(MesClient.plcC);
+        setPowerOn(MesClient.plcD);
         setTask(MesClient.plcA, MesClient.aSetNum, "A");
         setTask(MesClient.plcB, MesClient.bSetNum, "B");
+        setTask(MesClient.plcC, MesClient.cSetNum, "C");
+        setTask(MesClient.plcD, MesClient.dSetNum, "D");
         System.out.println("开工下发完成:A枪" + MesClient.aSetNum + "颗, B枪" + MesClient.bSetNum + "颗");
     }
 
@@ -301,6 +407,9 @@ public class ModbusUtil {
 
     // 重置任务
     public static void setTask(ModbusTcp plc, Short setNum, String gunType) {
+        if (plc == null) {
+            return;
+        }
         try{
             // 设置模式 1=标记铆接模式
             plc.writeInt16(1092,(short) 1);

+ 218 - 0
src/com/mes/update/ClientAutoUpdater.java

@@ -0,0 +1,218 @@
+package com.mes.update;
+
+import com.alibaba.fastjson2.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.Properties;
+
+public class ClientAutoUpdater {
+    private static final Logger log = LoggerFactory.getLogger(ClientAutoUpdater.class);
+
+    private ClientAutoUpdater() {
+    }
+
+    public static boolean checkAndUpdate(Properties config) {
+        try {
+            // 开关配置:现场调试时可在 config.properties 中设为 false,临时关闭自动升级。
+            if (!Boolean.parseBoolean(config.getProperty("mes.update.enabled", "true"))) {
+                return false;
+            }
+
+            // 从客户端配置读取服务器 IP、产线编号和工位号,服务端按这些参数匹配对应版本包。
+            String serverIp = trim(config.getProperty("mes.server_ip"));
+            String lineSn = trim(config.getProperty("mes.line_sn"));
+            String oprno = trim(config.getProperty("mes.gw"));
+            long currentVersion = getCurrentVersion(config);
+            if (serverIp.length() == 0 || lineSn.length() == 0 || oprno.length() == 0) {
+                return false;
+            }
+
+            // 调用服务端新建的“客户端版本上传”接口,返回是否有新版本、下载地址和版本号。
+            String apiUrl = "http://" + serverIp + ":8980/js/a/mes/mesClientUpgrade/checkUpdate"
+                    + "?currentVersion=" + currentVersion
+                    + "&lineSn=" + encode(lineSn)
+                    + "&oprno=" + encode(oprno);
+            String body = httpGet(apiUrl);
+            if (body == null || body.trim().length() == 0) {
+                return false;
+            }
+
+            JSONObject resp = JSONObject.parseObject(body);
+            if (!"true".equalsIgnoreCase(resp.getString("result"))) {
+                log.info("client update skipped: {}", resp.getString("message"));
+                return false;
+            }
+
+            JSONObject data = resp.getJSONObject("data");
+            if (data == null || !data.getBooleanValue("hasUpdate")) {
+                return false;
+            }
+
+            String downloadUrl = data.getString("downloadUrl");
+            long latestVersion = data.getLongValue("latestVersion");
+            if (downloadUrl == null || downloadUrl.length() == 0 || latestVersion <= currentVersion) {
+                return false;
+            }
+
+            // jarName 是要被替换的业务 jar,exeName 是更新后要重新启动的 exe 壳。
+            String jarName = config.getProperty("mes.update.jar", "mesclient-sd.jar");
+            String exeName = config.getProperty("mes.update.exe", "mesclient-sd.exe");
+            File appDir = resolveAppDir(jarName, exeName);
+            File updateDir = new File(appDir, ".update");
+            if (!updateDir.exists() && !updateDir.mkdirs()) {
+                log.warn("create update dir failed: {}", updateDir.getAbsolutePath());
+                return false;
+            }
+
+            // 先下载到 .update 临时目录,避免下载失败时破坏当前可运行的 jar。
+            File newJar = new File(updateDir, jarName + ".new");
+            download(downloadUrl, newJar);
+
+            File targetJar = new File(appDir, jarName);
+            File backupJar = new File(appDir, jarName + ".bak");
+            File launcherExe = new File(appDir, exeName);
+            File versionFile = new File(appDir, "client-version.properties");
+            File script = new File(updateDir, "update_mesclient.bat");
+            writeUpdateScript(script, newJar, targetJar, backupJar, launcherExe, versionFile, latestVersion);
+
+            // 当前 Java 进程不能稳定覆盖正在使用的 jar,所以启动 bat 后退出,由 bat 完成替换和重启。
+            Runtime.getRuntime().exec(new String[]{"cmd", "/c", "start", "\"MES Client Update\"", script.getAbsolutePath()});
+            log.info("client update started: {} -> {}", currentVersion, latestVersion);
+            System.exit(0);
+            return true;
+        } catch (Exception e) {
+            log.warn("client update check failed: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    private static long getCurrentVersion(Properties config) {
+        // 升级成功后会在 exe 同目录写入 client-version.properties,优先读取这个实际安装版本。
+        File versionFile = new File(System.getProperty("user.dir"), "client-version.properties");
+        if (versionFile.exists()) {
+            try (InputStream in = new FileInputStream(versionFile)) {
+                Properties versionProps = new Properties();
+                versionProps.load(in);
+                return Long.parseLong(versionProps.getProperty("mes.client.version", "0").trim());
+            } catch (Exception ignored) {
+            }
+        }
+        try {
+            // 第一次运行或没有版本文件时,使用 jar 内 config.properties 的初始版本号。
+            return Long.parseLong(config.getProperty("mes.client.version", "0").trim());
+        } catch (Exception e) {
+            return 0L;
+        }
+    }
+
+    private static File resolveAppDir(String jarName, String exeName) {
+        // 优先使用工作目录;如果快捷方式启动导致工作目录不对,再回退到当前 jar 所在目录。
+        File userDir = new File(System.getProperty("user.dir")).getAbsoluteFile();
+        if (new File(userDir, jarName).exists() || new File(userDir, exeName).exists()) {
+            return userDir;
+        }
+        try {
+            File codeFile = new File(ClientAutoUpdater.class.getProtectionDomain()
+                    .getCodeSource().getLocation().toURI()).getAbsoluteFile();
+            if (codeFile.isFile()) {
+                return codeFile.getParentFile();
+            }
+            return codeFile;
+        } catch (Exception e) {
+            return userDir;
+        }
+    }
+
+    private static String httpGet(String apiUrl) throws Exception {
+        // 简单 GET 请求,用于获取服务端版本检查 JSON。
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(apiUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(15000);
+            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                return null;
+            }
+            try (InputStream in = connection.getInputStream()) {
+                return readAll(in);
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    private static void download(String downloadUrl, File target) throws Exception {
+        // 下载新版 jar 到临时文件,下载完成后再由 bat 脚本替换正式 jar。
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(downloadUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(120000);
+            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+                throw new IllegalStateException("download failed, responseCode=" + connection.getResponseCode());
+            }
+            try (InputStream in = new BufferedInputStream(connection.getInputStream());
+                 BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(target))) {
+                byte[] buffer = new byte[8192];
+                int len;
+                while ((len = in.read(buffer)) != -1) {
+                    out.write(buffer, 0, len);
+                }
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    private static String readAll(InputStream in) throws Exception {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buffer = new byte[4096];
+        int len;
+        while ((len = in.read(buffer)) != -1) {
+            out.write(buffer, 0, len);
+        }
+        return new String(out.toByteArray(), "UTF-8");
+    }
+
+    private static void writeUpdateScript(File script, File newJar, File targetJar, File backupJar,
+                                          File launcherExe, File versionFile, long latestVersion) throws Exception {
+        try (BufferedWriter writer = new BufferedWriter(new FileWriter(script))) {
+            // bat 等待当前程序退出后,备份旧 jar、复制新 jar、写入版本号,并重新启动 exe。
+            writer.write("@echo off\r\n");
+            writer.write("chcp 65001 >nul\r\n");
+            writer.write("timeout /t 2 /nobreak >nul\r\n");
+            writer.write("if exist \"" + targetJar.getAbsolutePath() + "\" copy /Y \"" + targetJar.getAbsolutePath() + "\" \"" + backupJar.getAbsolutePath() + "\" >nul\r\n");
+            writer.write("copy /Y \"" + newJar.getAbsolutePath() + "\" \"" + targetJar.getAbsolutePath() + "\" >nul\r\n");
+            writer.write("if errorlevel 1 (\r\n");
+            writer.write("  if exist \"" + backupJar.getAbsolutePath() + "\" copy /Y \"" + backupJar.getAbsolutePath() + "\" \"" + targetJar.getAbsolutePath() + "\" >nul\r\n");
+            writer.write("  exit /b 1\r\n");
+            writer.write(")\r\n");
+            writer.write("echo mes.client.version=" + latestVersion + ">\"" + versionFile.getAbsolutePath() + "\"\r\n");
+            writer.write("if exist \"" + launcherExe.getAbsolutePath() + "\" (\r\n");
+            writer.write("  start \"\" \"" + launcherExe.getAbsolutePath() + "\"\r\n");
+            writer.write(") else (\r\n");
+            writer.write("  start \"\" javaw -jar \"" + targetJar.getAbsolutePath() + "\"\r\n");
+            writer.write(")\r\n");
+        }
+    }
+
+    private static String encode(String value) throws Exception {
+        return URLEncoder.encode(value, "UTF-8");
+    }
+
+    private static String trim(String value) {
+        return value == null ? "" : value.trim();
+    }
+}

+ 31 - 4
src/com/mes/util/HttpUtils.java

@@ -1,9 +1,6 @@
 package com.mes.util;
 
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
+import java.io.*;
 import java.net.HttpURLConnection;
 import java.net.URISyntaxException;
 import java.net.URL;
@@ -14,6 +11,36 @@ public class HttpUtils {
 	 * @return 
 	 * @throws URISyntaxException
 	 */
+    public static String sendPostRequestJson(String apiUrl, String jsonData) throws IOException {
+        URL url = new URL(apiUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        // 发送请求数据
+        try (OutputStream os = conn.getOutputStream()) {
+            byte[] input = jsonData.getBytes("utf-8");
+            os.write(input, 0, input.length);
+        }
+
+        // 获取响应数据
+        StringBuilder response = new StringBuilder();
+        int responseCode = conn.getResponseCode();
+//        System.out.println("Sending POST request to URL: " + apiUrl);
+//        System.out.println("Post parameters: " + jsonData);
+//        System.out.println("Response Code: " + responseCode);
+
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
+            String responseLine;
+            while ((responseLine = br.readLine()) != null) {
+                response.append(responseLine.trim());
+            }
+        }
+
+        conn.disconnect();
+        return response.toString();
+    }
     //http post请求
     public static String sendRequest(String urlParam) {
 		String requestType = "POST";

+ 54 - 10
src/com/mes/util/JdbcUtils.java

@@ -92,32 +92,68 @@ public class JdbcUtils {
 				+ "id INTEGER PRIMARY KEY AUTOINCREMENT, "
 				+ "a_set_num SHORT, "
 				+ "b_set_num SHORT, "
+				+ "c_set_num SHORT, "
+				+ "d_set_num SHORT, "
 				+ "plc_ip_a VARCHAR(20), "
-				+ "plc_ip_b VARCHAR(20))";
+				+ "plc_ip_b VARCHAR(20), "
+				+ "plc_ip_c VARCHAR(20), "
+				+ "plc_ip_d VARCHAR(20))";
 		statement.executeUpdate(sql);
+		ensureConfigColumn(statement, "c_set_num", "SHORT", "14");
+		ensureConfigColumn(statement, "d_set_num", "SHORT", "14");
+		ensureConfigColumn(statement, "plc_ip_c", "VARCHAR(20)", "'192.168.1.9'");
+		ensureConfigColumn(statement, "plc_ip_d", "VARCHAR(20)", "'192.168.1.10'");
 		
 		// 检查是否已有数据,若无则插入默认值
 		ResultSet rs = statement.executeQuery("SELECT count(*) FROM bw_config");
-		if (rs.next() && rs.getInt(1) == 0) {
-			statement.executeUpdate("INSERT INTO bw_config (a_set_num, b_set_num, plc_ip_a, plc_ip_b) "
-					+ "VALUES (41, 14, '192.168.1.7', '192.168.1.8')");
+		int configCount = 0;
+		if (rs.next()) {
+			configCount = rs.getInt(1);
 		}
 		rs.close();
+		if (configCount == 0) {
+			statement.executeUpdate("INSERT INTO bw_config (a_set_num, b_set_num, c_set_num, d_set_num, plc_ip_a, plc_ip_b, plc_ip_c, plc_ip_d) "
+					+ "VALUES (41, 14, 14, 14, '192.168.1.7', '192.168.1.8', '192.168.1.9', '192.168.1.10')");
+		} else {
+			statement.executeUpdate("UPDATE bw_config SET c_set_num = 14 WHERE c_set_num IS NULL");
+			statement.executeUpdate("UPDATE bw_config SET d_set_num = 14 WHERE d_set_num IS NULL");
+			statement.executeUpdate("UPDATE bw_config SET plc_ip_c = '192.168.1.9' WHERE plc_ip_c IS NULL OR plc_ip_c = ''");
+			statement.executeUpdate("UPDATE bw_config SET plc_ip_d = '192.168.1.10' WHERE plc_ip_d IS NULL OR plc_ip_d = ''");
+		}
 		System.out.println("表config创建并初始化成功!");
 		statement.close();
 	}
 
+	private static void ensureConfigColumn(Statement statement, String columnName, String columnType, String defaultValue) throws SQLException {
+		ResultSet rs = statement.executeQuery("PRAGMA table_info(bw_config)");
+		boolean exists = false;
+		while (rs.next()) {
+			if (columnName.equalsIgnoreCase(rs.getString("name"))) {
+				exists = true;
+				break;
+			}
+		}
+		rs.close();
+		if (!exists) {
+			statement.executeUpdate("ALTER TABLE bw_config ADD COLUMN " + columnName + " " + columnType + " DEFAULT " + defaultValue);
+		}
+	}
+
 	public static java.util.Map<String, Object> getConfig() {
 		java.util.Map<String, Object> config = new java.util.HashMap<>();
-		String sql = "SELECT a_set_num, b_set_num, plc_ip_a, plc_ip_b FROM bw_config LIMIT 1";
+		String sql = "SELECT a_set_num, b_set_num, c_set_num, d_set_num, plc_ip_a, plc_ip_b, plc_ip_c, plc_ip_d FROM bw_config LIMIT 1";
 		Connection conn = JdbcUtils.getConn();
 		try (Statement stmt = conn.createStatement();
 			 ResultSet rs = stmt.executeQuery(sql)) {
 			if (rs.next()) {
 				config.put("a_set_num", rs.getShort("a_set_num"));
 				config.put("b_set_num", rs.getShort("b_set_num"));
+				config.put("c_set_num", rs.getShort("c_set_num"));
+				config.put("d_set_num", rs.getShort("d_set_num"));
 				config.put("plc_ip_a", rs.getString("plc_ip_a"));
 				config.put("plc_ip_b", rs.getString("plc_ip_b"));
+				config.put("plc_ip_c", rs.getString("plc_ip_c"));
+				config.put("plc_ip_d", rs.getString("plc_ip_d"));
 			}
 		} catch (SQLException e) {
 			e.printStackTrace();
@@ -144,7 +180,9 @@ public class JdbcUtils {
 		if (rs.next() && rs.getInt(1) == 0) {
 			statement.executeUpdate("INSERT INTO bw_rivet_param (gun_type,start_rivet,end_rivet,f_min,f_max,s_min,s_max,sort_order) VALUES "
 					+ "('A',1,41,10000,12000,3500,5500,1),"
-					+ "('B',1,14,11000,13000,3500,5500,1)");
+					+ "('B',1,14,11000,13000,3500,5500,1),"
+					+ "('C',1,14,11000,13000,3500,5500,1),"
+					+ "('D',1,14,11000,13000,3500,5500,1)");
 		}
 		rs.close();
 		System.out.println("表rivet_param创建并初始化成功!");
@@ -228,8 +266,10 @@ public class JdbcUtils {
 		}
 	}
 
-	public static synchronized boolean updateConfig(short aSetNum, short bSetNum, String plcIpA, String plcIpB) {
-		String sql = "UPDATE bw_config SET a_set_num = ?, b_set_num = ?, plc_ip_a = ?, plc_ip_b = ? WHERE id = 1";
+	public static synchronized boolean updateConfig(short aSetNum, short bSetNum, short cSetNum, short dSetNum,
+			String plcIpA, String plcIpB, String plcIpC, String plcIpD) {
+		String sql = "UPDATE bw_config SET a_set_num = ?, b_set_num = ?, c_set_num = ?, d_set_num = ?, "
+				+ "plc_ip_a = ?, plc_ip_b = ?, plc_ip_c = ?, plc_ip_d = ? WHERE id = 1";
 		Connection conn = JdbcUtils.getConn();
 		if (conn == null) {
 			System.out.println("更新本地配置失败:数据库未连接");
@@ -238,8 +278,12 @@ public class JdbcUtils {
 		try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
 			pstmt.setShort(1, aSetNum);
 			pstmt.setShort(2, bSetNum);
-			pstmt.setString(3, plcIpA);
-			pstmt.setString(4, plcIpB);
+			pstmt.setShort(3, cSetNum);
+			pstmt.setShort(4, dSetNum);
+			pstmt.setString(5, plcIpA);
+			pstmt.setString(6, plcIpB);
+			pstmt.setString(7, plcIpC);
+			pstmt.setString(8, plcIpD);
 			int rows = pstmt.executeUpdate();
 			if (rows <= 0) {
 				System.out.println("更新本地配置失败:未更新到任何行");

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

@@ -3,4 +3,13 @@ mes.gw=OP210A
 mes.server_ip=192.168.9.180
 mes.tcp_port=3000
 mes.heart_beat_cycle=60
-mes.line_sn=HEVXT
+mes.line_sn=HEVXT
+mes.device.heartbeat.minutes=5
+# \u00E5\u00AE\u00A2\u00E6\u0088\u00B7\u00E7\u00AB\u00AF\u00E7\u0089\u0088\u00E6\u009C\u00AC\u00E5\u008F\u00B7\u00EF\u00BC\u009B\u00E5\u008D\u0087\u00E7\u00BA\u00A7\u00E7\u00A8\u008B\u00E5\u00BA\u008F\u00E4\u00B9\u009F\u00E5\u008F\u00AF\u00E4\u00BB\u00A5\u00E8\u00AF\u00BB\u00E5\u008F\u0096 client-version.properties
+mes.client.version=100
+# \u00E6\u0098\u00AF\u00E5\u0090\u00A6\u00E5\u0090\u00AF\u00E7\u0094\u00A8\u00E5\u00AE\u00A2\u00E6\u0088\u00B7\u00E7\u00AB\u00AF\u00E6\u009B\u00B4\u00E6\u0096\u00B0\u00E6\u00A3\u0080\u00E6\u009F\u00A5
+mes.update.enabled=true
+# exe \u00E5\u0090\u00AF\u00E5\u008A\u00A8\u00E5\u0099\u00A8 classpath \u00E4\u00BD\u00BF\u00E7\u0094\u00A8\u00E7\u009A\u0084\u00E4\u00B8\u009A\u00E5\u008A\u00A1 jar \u00E5\u0090\u008D\u00E7\u00A7\u00B0
+mes.update.jar=mesclient-sd.jar
+# \u00E6\u009B\u00B4\u00E6\u0096\u00B0\u00E5\u00AE\u008C\u00E6\u0088\u0090\u00E5\u0090\u008E\u00E9\u0087\u008D\u00E6\u0096\u00B0\u00E5\u0090\u00AF\u00E5\u008A\u00A8\u00E7\u009A\u0084 exe \u00E5\u0090\u008D\u00E7\u00A7\u00B0
+mes.update.exe=mesclient-sd.exe