chefz 2 هفته پیش
والد
کامیت
9774cd120f

+ 358 - 142
src/com/mes/component/WorkJPanelDevice.java

@@ -50,6 +50,11 @@ public class WorkJPanelDevice extends JPanel {
 
     public JLabel blankingmeslabel; // 下料MES反馈
 
+    /** 当班离线模式开关(仅内存,重启默认在线) */
+    public JLabel offlineModeLabel;
+    /** true=离线:跳过校验与服务端提交,仍读写 PLC */
+    public boolean offlineMode = false;
+
     public JButton scanBtn;
 
     public JTextField productSn;
@@ -72,9 +77,11 @@ public class WorkJPanelDevice extends JPanel {
 
     public ImageIcon imageGray;
 
+    public ImageIcon imageRed;
+
     public String plcIp = "192.168.15.201";
 
-    /** OP190 拉铆枪 PLC(与机器人 PLC 分离) */
+    /** OP190 拉铆枪 PLC(一拖二共用一台 S1200:A=DB40/41,B=DB50/51) */
     public String rivetingPlcIp = "192.168.0.200";
 
     public String deviceIp = "192.168.88.99";
@@ -122,10 +129,11 @@ public class WorkJPanelDevice extends JPanel {
 
     public  JLabel oprnoLabel;
 
-    // 拉铆预设数量文本框
+    // 拉铆 A/B 枪预设、完成数量(一拖二,同 LM-1-2)
     public JTextField presetField;
-    // 拉铆完成数量文本框
     public JTextField finishField;
+    public JTextField presetFieldB;
+    public JTextField finishFieldB;
 
     // OP190 冷板绑定
     public JTextField productSnLb;
@@ -133,17 +141,23 @@ public class WorkJPanelDevice extends JPanel {
     public JButton btnUnbindLb;
     public boolean bindLbStatus = false;
     private static final String LB_CRAFT = "400006";
-    /** 测试开关:true=OP190 测试模式(固定工件码、模拟上料/运行、拉铆直接满16并走完工流程;测试完改回 false) */
+    /** 测试开关:true=OP190 测试模式(固定工件码、模拟上料/运行、双枪打满并走完工流程;测试完改回 false) */
     private static final boolean OP190_TEST_SN_ENABLED = false;
     private static final String OP190_TEST_SN = "000020022144-0100101426053100009";
-    /** OP190 拉铆打满后是否已触发完工提交,防止重复提交 */
+    /** OP190 双枪都打满后是否已触发完工提交,防止重复提交 */
     private volatile boolean op190RivetingComplete = false;
     /** OP190 质量结果是否已异步提交(拉铆完成时提交,下料时不再重复) */
     private volatile boolean op190QualitySubmitted = false;
+    /** 双枪均满后是否已关气,避免重复写 PLC */
+    private volatile boolean bothGunsPoweredOff = false;
 
-    public int aMax = 16; // A枪预设总数
-    public int sortA = 0; // A枪当前已读取的数量
-    public int aFinish = 0; // A枪合格数量
+    /** A/B 枪预设(各 8 颗;同一台拉铆 PLC:A=DB40/41,B=DB50/51) */
+    public int aMax = 8;
+    public int bMax = 8;
+    public int sortA = 0;
+    public int sortB = 0;
+    public int aFinish = 0;
+    public int bFinish = 0;
 
     // OP170
 //    public int[] toolNums = { 1,2,3,4,5};
@@ -447,49 +461,35 @@ public class WorkJPanelDevice extends JPanel {
             }
         });
         this.add(blankingmeslabel);
+
+        offlineModeLabel = createIndicatorLabel("离线模式", indicatorFont);
+        offlineModeLabel.setBounds(contentX + colW * 2, y, colW, indicatorRowH);
+        offlineModeLabel.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                toggleOfflineMode();
+            }
+        });
+        this.add(offlineModeLabel);
+        refreshOfflineModeUi();
         y += indicatorRowH + gap;
 
-        int rivetPanelH = 72;
-        JPanel rivetPanel = new JPanel(new GridLayout(1, 4, 16, 0));
+        int rivetPanelH = 150;
+        JPanel rivetPanel = new JPanel(new GridLayout(1, 2, 20, 0));
         rivetPanel.setBackground(new Color(248, 249, 252));
         rivetPanel.setBorder(BorderFactory.createTitledBorder(
                 BorderFactory.createLineBorder(new Color(190, 195, 205)),
-                "拉铆进度",
+                "拉铆进度(一拖二)",
                 TitledBorder.CENTER,
                 TitledBorder.TOP,
                 new Font("微软雅黑", Font.BOLD, 16),
                 new Color(70, 70, 70)));
 
-        Font statLabelFont = new Font("微软雅黑", Font.PLAIN, 18);
-        Font statValueFont = new Font("微软雅黑", Font.BOLD, 32);
+        Font gunTitleFont = new Font("微软雅黑", Font.BOLD, 18);
+        Font statLabelFont = new Font("微软雅黑", Font.PLAIN, 16);
+        Font statValueFont = new Font("微软雅黑", Font.BOLD, 28);
 
-        JLabel lblPreset = new JLabel("预设数量", SwingConstants.CENTER);
-        lblPreset.setFont(statLabelFont);
-        lblPreset.setForeground(new Color(80, 80, 80));
-        rivetPanel.add(lblPreset);
-
-        presetField = new JTextField(String.valueOf(aMax));
-        presetField.setFont(statValueFont);
-        presetField.setEditable(false);
-        presetField.setBorder(BorderFactory.createEmptyBorder());
-        presetField.setBackground(new Color(248, 249, 252));
-        presetField.setForeground(new Color(30, 100, 180));
-        presetField.setHorizontalAlignment(SwingConstants.CENTER);
-        rivetPanel.add(presetField);
-
-        JLabel lblFinish = new JLabel("完成数量", SwingConstants.CENTER);
-        lblFinish.setFont(statLabelFont);
-        lblFinish.setForeground(new Color(80, 80, 80));
-        rivetPanel.add(lblFinish);
-
-        finishField = new JTextField("0");
-        finishField.setFont(statValueFont);
-        finishField.setEditable(false);
-        finishField.setBorder(BorderFactory.createEmptyBorder());
-        finishField.setBackground(new Color(248, 249, 252));
-        finishField.setForeground(new Color(30, 130, 70));
-        finishField.setHorizontalAlignment(SwingConstants.CENTER);
-        rivetPanel.add(finishField);
+        rivetPanel.add(buildGunProgressPanel("A枪", gunTitleFont, statLabelFont, statValueFont, true));
+        rivetPanel.add(buildGunProgressPanel("B枪", gunTitleFont, statLabelFont, statValueFont, false));
 
         rivetPanel.setBounds(contentX, y, contentW, rivetPanelH);
         this.add(rivetPanel);
@@ -515,11 +515,71 @@ public class WorkJPanelDevice extends JPanel {
         return field;
     }
 
+    /** OP190 单枪进度子面板:标题 + 预设/完成 */
+    private JPanel buildGunProgressPanel(String title, Font titleFont, Font labelFont, Font valueFont, boolean gunA) {
+        JPanel gun = new JPanel(new BorderLayout(0, 6));
+        gun.setOpaque(false);
+        JLabel titleLbl = new JLabel(title, SwingConstants.CENTER);
+        titleLbl.setFont(titleFont);
+        titleLbl.setForeground(new Color(50, 50, 50));
+        gun.add(titleLbl, BorderLayout.NORTH);
+
+        JPanel stats = new JPanel(new GridLayout(2, 2, 8, 4));
+        stats.setOpaque(false);
+        JLabel lblPreset = new JLabel("预设", SwingConstants.CENTER);
+        lblPreset.setFont(labelFont);
+        lblPreset.setForeground(new Color(80, 80, 80));
+        stats.add(lblPreset);
+        JLabel lblFinish = new JLabel("完成", SwingConstants.CENTER);
+        lblFinish.setFont(labelFont);
+        lblFinish.setForeground(new Color(80, 80, 80));
+        stats.add(lblFinish);
+
+        JTextField preset = new JTextField(String.valueOf(gunA ? aMax : bMax));
+        preset.setFont(valueFont);
+        preset.setEditable(false);
+        preset.setBorder(BorderFactory.createEmptyBorder());
+        preset.setBackground(new Color(248, 249, 252));
+        preset.setForeground(new Color(30, 100, 180));
+        preset.setHorizontalAlignment(SwingConstants.CENTER);
+        stats.add(preset);
+
+        JTextField finish = new JTextField("0");
+        finish.setFont(valueFont);
+        finish.setEditable(false);
+        finish.setBorder(BorderFactory.createEmptyBorder());
+        finish.setBackground(new Color(248, 249, 252));
+        finish.setForeground(new Color(30, 130, 70));
+        finish.setHorizontalAlignment(SwingConstants.CENTER);
+        stats.add(finish);
+
+        if (gunA) {
+            presetField = preset;
+            finishField = finish;
+        } else {
+            presetFieldB = preset;
+            finishFieldB = finish;
+        }
+        gun.add(stats, BorderLayout.CENTER);
+        return gun;
+    }
+
     private void initOp190IndicatorIcons() {
         imageGreen = new ImageIcon(MesClient.class.getResource("/bg/green_dot.png"));
         imageGreen.setImage(imageGreen.getImage().getScaledInstance(18, 18, Image.SCALE_SMOOTH));
         imageGray = new ImageIcon(MesClient.class.getResource("/bg/grey_dot.png"));
         imageGray.setImage(imageGray.getImage().getScaledInstance(18, 18, Image.SCALE_SMOOTH));
+        imageRed = createColorDotIcon(Color.RED, 18);
+    }
+
+    private ImageIcon createColorDotIcon(Color color, int size) {
+        java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(size, size, java.awt.image.BufferedImage.TYPE_INT_ARGB);
+        Graphics2D g = img.createGraphics();
+        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+        g.setColor(color);
+        g.fillOval(1, 1, size - 2, size - 2);
+        g.dispose();
+        return new ImageIcon(img);
     }
 
     private JLabel createIndicatorLabel(String text, Font font) {
@@ -530,6 +590,52 @@ public class WorkJPanelDevice extends JPanel {
         return label;
     }
 
+    /** 密码确认后切换当班离线模式(仅内存,重启默认在线) */
+    private void toggleOfflineMode() {
+        String input = JOptionPane.showInputDialog(null, "请输入密码", "离线模式", JOptionPane.QUESTION_MESSAGE);
+        if (input == null) {
+            return;
+        }
+        if (!"mes".equals(input.trim())) {
+            JOptionPane.showMessageDialog(null, "密码错误", "离线模式", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+        offlineMode = !offlineMode;
+        refreshOfflineModeUi();
+        log.info("{}{} 离线模式切换为: {}", oprno, pos, offlineMode);
+        if (offlineMode) {
+            setMenuStatus("[离线] 已开启,读码后不校验不提交", 0);
+        } else {
+            setMenuStatus("已切回在线模式", 0);
+        }
+    }
+
+    private void refreshOfflineModeUi() {
+        if (offlineModeLabel == null) {
+            return;
+        }
+        if (offlineMode) {
+            offlineModeLabel.setIcon(imageRed != null ? imageRed : imageGray);
+            offlineModeLabel.setForeground(Color.RED);
+        } else {
+            offlineModeLabel.setIcon(imageGray);
+            offlineModeLabel.setForeground(Color.BLACK);
+        }
+    }
+
+    public void setMenuStatus(String msg, int error) {
+        String text = msg == null ? "" : msg;
+        if (offlineMode && !text.startsWith("[离线]")) {
+            text = "[离线] " + text;
+        }
+        if (error == 0) {
+            this.status_menu.setForeground(Color.GREEN);
+        } else {
+            this.status_menu.setForeground(Color.RED);
+        }
+        this.status_menu.setText(text);
+    }
+
     /**
      * 非 OP190 工位默认界面
      */
@@ -679,6 +785,7 @@ public class WorkJPanelDevice extends JPanel {
 
         imageGray = new ImageIcon(MesClient.class.getResource("/bg/grey_dot.png"));
         imageGray.setImage(imageGray.getImage().getScaledInstance((int)(20 * scale), (int)(20 * scale), Image.SCALE_SMOOTH));
+        imageRed = createColorDotIcon(Color.RED, (int)(20 * scale));
 
         int feedingIndicatorY = (int)(120 * scale);
         int blankingIndicatorY = (int)(160 * scale);
@@ -741,6 +848,20 @@ public class WorkJPanelDevice extends JPanel {
                 }
             }
         });
+
+        offlineModeLabel = new JLabel("离线模式");
+        offlineModeLabel.setHorizontalAlignment(SwingConstants.LEFT);
+        offlineModeLabel.setIcon(imageGray);
+        offlineModeLabel.setForeground(Color.BLACK);
+        offlineModeLabel.setFont(indicatorFont);
+        offlineModeLabel.setBounds(controlWidth*2/3, blankingIndicatorY, controlWidth/3, (int)(30 * scale));
+        offlineModeLabel.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                toggleOfflineMode();
+            }
+        });
+        this.add(offlineModeLabel);
+        refreshOfflineModeUi();
     }
 
     private void showCheckDialog() {
@@ -769,15 +890,6 @@ public class WorkJPanelDevice extends JPanel {
         System.out.println("Dialog opened");
     }
 
-    public void setMenuStatus(String msg,int error){
-        if(error == 0){
-            this.status_menu.setForeground(Color.GREEN);
-        }else{
-            this.status_menu.setForeground(Color.RED);
-        }
-        this.status_menu.setText(msg);
-    }
-
     // 定时任务 -----------------------------------------------------------------------------------------------------
 
     public Timer rivetingTimer;
@@ -793,9 +905,9 @@ public class WorkJPanelDevice extends JPanel {
         rivetingTimer.schedule(new TimerTask() {
             @Override
             public void run() {
-                // 仅在已登录、非本地模式、已绑定冷板(OP190)且处于加工阶段时轮询
+                // 仅在已登录、非本地模式;OP190 需已绑冷板或离线模式
                 if (MesClient.isLogin == 1 && isLocal == 0 && tjFeedingFlag >= 1
-                        && (!oprno.equals("OP190") || bindLbStatus)) {
+                        && (!oprno.equals("OP190") || bindLbStatus || offlineMode)) {
                     try {
                         pollRivetingData();
                     } catch (Exception e) {
@@ -814,111 +926,162 @@ public class WorkJPanelDevice extends JPanel {
     }
 
     /**
-     * 轮询读取拉铆实时数据
-     * 获取拉力、行程等参数并保存至数据库
+     * 轮询读取双枪拉铆实时数据(一拖二:A=DB40/41,B=DB50/51,无复判)
      */
     private void pollRivetingData() {
         if (oprno.equals("OP190") && OP190_TEST_SN_ENABLED) {
-            // 测试模式:绑定冷板进入加工后,拉铆完成数直接变为16并触发完工
             if (tjFeedingFlag >= 2 && bindLbStatus) {
                 simulateOp190RivetingComplete();
             }
             return;
         }
         if (getRivetingPlc() == null) return;
-        
+
         S7PLC rivetingPlc = getRivetingPlc();
-        // 读取 PLC 当前打钉数
-        int cur = rivetingPlc.readInt32("DB41.32");
-        if (presetField != null) {
-            presetField.setText(String.valueOf(aMax));
-        }
-        
-        // 当打钉数增加时,读取对应参数
-        if (sortA < cur) {
-            sortA = cur;
-            if (cur == 9) {
-                rivetingPlc.writeUInt16("DB40.2", 2);
+        pollOneGun(rivetingPlc, true);
+        pollOneGun(rivetingPlc, false);
+        syncDualGunPower(rivetingPlc);
+    }
+
+    /**
+     * 读取单枪增量数据并落库。gunA=true 用 DB40/41,否则 DB50/51。
+     */
+    private void pollOneGun(S7PLC rivetingPlc, boolean gunA) {
+        String dbCtrl = gunA ? "DB40" : "DB50";
+        String dbData = gunA ? "DB41" : "DB51";
+        int max = gunA ? aMax : bMax;
+        int last = gunA ? sortA : sortB;
+
+        int cur = rivetingPlc.readInt32(dbData + ".32");
+        if (gunA) {
+            if (presetField != null) {
+                presetField.setText(String.valueOf(aMax));
             }
-            
-            // 读取拉力和行程
-            int fout = rivetingPlc.readInt32("DB41.24");
-            Float sout = rivetingPlc.readFloat32("DB41.28");
+        } else if (presetFieldB != null) {
+            presetFieldB.setText(String.valueOf(bMax));
+        }
 
-            int fmin = rivetingPlc.readInt32("DB41.0") - rivetingPlc.readInt32("DB41.8");
-            Float smin = rivetingPlc.readFloat32("DB41.12") - rivetingPlc.readFloat32("DB41.20");
+        if (last >= cur) {
+            return;
+        }
 
-            int fmax = rivetingPlc.readInt32("DB41.0") + rivetingPlc.readInt32("DB41.4");
-            Float smax = rivetingPlc.readInt32("DB41.12") + rivetingPlc.readFloat32("DB41.16");
+        if (gunA) {
+            sortA = cur;
+        } else {
+            sortB = cur;
+        }
 
-            String qty = "0";
-            String resultDing = "OK";
+        // A 枪历史逻辑:第 9 颗切换任务模式
+        if (gunA && cur == 9) {
             try {
-                // 读取判定状态,0为NG
-                byte retang = rivetingPlc.readByte("DB41.36");
-                if ((int) retang == 0) {
-                    resultDing = "NG";
-                }
+                rivetingPlc.writeUInt16(dbCtrl + ".2", 2);
             } catch (Exception e) {
-                resultDing = "NG";
+                log.warn("写拉铆任务模式失败 gun={}", gunA ? "A" : "B", e);
             }
-            
-            if ("OK".equals(resultDing)) {
-                qty = "1";
+        }
+
+        int fout = rivetingPlc.readInt32(dbData + ".24");
+        Float sout = rivetingPlc.readFloat32(dbData + ".28");
+        int fmin = rivetingPlc.readInt32(dbData + ".0") - rivetingPlc.readInt32(dbData + ".8");
+        Float smin = rivetingPlc.readFloat32(dbData + ".12") - rivetingPlc.readFloat32(dbData + ".20");
+        int fmax = rivetingPlc.readInt32(dbData + ".0") + rivetingPlc.readInt32(dbData + ".4");
+        Float smax = rivetingPlc.readFloat32(dbData + ".12") + rivetingPlc.readFloat32(dbData + ".16");
+
+        String qty = "0";
+        String resultDing = "OK";
+        try {
+            byte retang = rivetingPlc.readByte(dbData + ".36");
+            if ((int) retang == 0) {
+                resultDing = "NG";
             }
-            // 单颗 NG 也计入完成数量,避免 NG 后 aFinish 不足导致流程卡住
+        } catch (Exception e) {
+            resultDing = "NG";
+        }
+        if ("OK".equals(resultDing)) {
+            qty = "1";
+        }
+        // 单颗 NG 也计入完成数,避免流程卡住(无复判弹窗)
+        if (gunA) {
             aFinish++;
+        } else {
+            bFinish++;
+        }
 
-            // 格式化数据以展示并保存
-            java.text.DecimalFormat decimalFormat = new java.text.DecimalFormat("#.###");
-            String soutstr = decimalFormat.format(sout);
-            String sminstr = decimalFormat.format(smin);
-            String smaxstr = decimalFormat.format(smax);
-            String foutstr = fout + "";
-            String fminstr = fmin + "";
-            String fmaxstr = fmax + "";
+        java.text.DecimalFormat decimalFormat = new java.text.DecimalFormat("#.###");
+        String soutstr = decimalFormat.format(sout);
+        String sminstr = decimalFormat.format(smin);
+        String smaxstr = decimalFormat.format(smax);
+        String foutstr = fout + "";
+        String fminstr = fmin + "";
+        String fmaxstr = fmax + "";
 
-            // 更新 UI 界面(仅完成数量)
+        if (gunA) {
             if (finishField != null) {
-                finishField.setText(cur + "");
-            }
-            
-            log.info("OP190 拉铆数据 -> cur: {}, fout: {}, sout: {}", cur, foutstr, soutstr);
-            
-            // 将单颗螺钉数据保存到本地 SQLite,等待批量上传
-            if (!productSnFeeding.getText().isEmpty()) {
-                JSONObject obj = new JSONObject();
-                obj.put("pos", pos);
-                obj.put("foutstr", foutstr);
-                obj.put("soutstr", soutstr);
-                obj.put("fminstr", fminstr);
-                obj.put("sminstr", sminstr);
-                obj.put("fmaxstr", fmaxstr);
-                obj.put("smaxstr", smaxstr);
-                obj.put("qty", qty);
-                obj.put("cur", cur + "");
-                obj.put("user", MesClient.user_menu.getText());
-                JdbcUtils.insertParamData(oprno, MesClient.mes_line_sn, productSnFeeding.getText(), obj.toJSONString());
+                finishField.setText(String.valueOf(Math.min(cur, max)));
             }
+        } else if (finishFieldB != null) {
+            finishFieldB.setText(String.valueOf(Math.min(cur, max)));
         }
-        
-        // 当完成数量达到预设时:关电源 + OP190 立即异步提交质量;下料信号仅做 MES 反馈
-        if (cur == aMax) {
-            powerOffRiveting();
+
+        String gun = gunA ? "A" : "B";
+        log.info("OP190 拉铆数据 gun={} -> cur: {}, fout: {}, sout: {}", gun, cur, foutstr, soutstr);
+
+        if (!productSnFeeding.getText().isEmpty() && cur >= 1 && (max <= 0 || cur <= max)) {
+            JdbcUtils.insertProdData(
+                    oprno,
+                    MesClient.mes_line_sn,
+                    productSnFeeding.getText(),
+                    gun,
+                    foutstr, soutstr, fminstr, sminstr, fmaxstr, smaxstr,
+                    qty, cur + "", MesClient.user_menu.getText());
+        }
+    }
+
+    /**
+     * 双枪气路同步:单枪打满关该枪;双枪都满关总气并提交质量;未满意外断气则重开。
+     */
+    private void syncDualGunPower(S7PLC rivetingPlc) {
+        boolean aDone = sortA >= aMax;
+        boolean bDone = sortB >= bMax;
+
+        if (aDone && bDone) {
+            if (!bothGunsPoweredOff) {
+                powerOffRiveting();
+                bothGunsPoweredOff = true;
+            }
             if (oprno.equals("OP190") && !op190RivetingComplete) {
                 op190RivetingComplete = true;
-                submitOp190QualityAsync();
+                if (!offlineMode) {
+                    submitOp190QualityAsync();
+                } else {
+                    setMenuStatus("双枪拉铆完成(离线不提交),等待下料", 0);
+                    log.info("OP190 离线模式:双枪拉铆完成,跳过质量提交");
+                }
             }
-        } else if (bindLbStatus && tjFeedingFlag >= 1 && sortA < aMax) {
-            // 绑定冷板后,若拉铆机意外停机则尝试恢复
-            try {
+            return;
+        }
+
+        bothGunsPoweredOff = false;
+        boolean canRecover = (bindLbStatus || offlineMode) && tjFeedingFlag >= 1;
+        try {
+            if (aDone) {
+                rivetingPlc.writeBoolean("DB40.0.0", false);
+            } else if (canRecover) {
                 boolean powerOn = rivetingPlc.readBoolean("DB40.0.0");
                 if (!powerOn) {
                     rivetingPlc.writeBoolean("DB40.0.0", true);
                 }
-            } catch (Exception e) {
-                log.error("恢复拉铆机电源失败", e);
             }
+            if (bDone) {
+                rivetingPlc.writeBoolean("DB50.0.0", false);
+            } else if (canRecover) {
+                boolean powerOn = rivetingPlc.readBoolean("DB50.0.0");
+                if (!powerOn) {
+                    rivetingPlc.writeBoolean("DB50.0.0", true);
+                }
+            }
+        } catch (Exception e) {
+            log.error("同步双枪气路失败", e);
         }
     }
 
@@ -926,6 +1089,10 @@ public class WorkJPanelDevice extends JPanel {
      * OP190:拉铆完成后立即异步提交质量结果,不阻塞等待下料信号。
      */
     private void submitOp190QualityAsync() {
+        if (offlineMode) {
+            log.info("OP190 离线模式:跳过质量提交");
+            return;
+        }
         if (op190QualitySubmitted) {
             return;
         }
@@ -941,29 +1108,38 @@ public class WorkJPanelDevice extends JPanel {
         final String user = MesClient.user_menu.getText();
         // 与 readResult() 一致:OP190 暂固定 OK
         DataUtil.submitQualityAsync(sn, station, "OK", user);
-        setMenuStatus("拉铆完成,结果后台提交中,等待下料", 0);
-        log.info("OP190 拉铆完成,已异步提交质量 sn={}", sn);
+        setMenuStatus("双枪拉铆完成,结果后台提交中,等待下料", 0);
+        log.info("OP190 双枪拉铆完成,已异步提交质量 sn={}", sn);
     }
 
-    /** OP190 测试:拉铆完成数直接变为预设值,并立即异步提交质量 */
+    /** OP190 测试:双枪完成数直接变为预设值,并立即异步提交质量 */
     private void simulateOp190RivetingComplete() {
         if (op190RivetingComplete) {
             return;
         }
         sortA = aMax;
+        sortB = bMax;
         aFinish = aMax;
+        bFinish = bMax;
         if (presetField != null) {
             presetField.setText(String.valueOf(aMax));
         }
         if (finishField != null) {
             finishField.setText(String.valueOf(aMax));
         }
-        log.info("OP190 测试模式:拉铆完成数模拟为 {}/{}", aMax, aMax);
+        if (presetFieldB != null) {
+            presetFieldB.setText(String.valueOf(bMax));
+        }
+        if (finishFieldB != null) {
+            finishFieldB.setText(String.valueOf(bMax));
+        }
+        log.info("OP190 测试模式:双枪拉铆完成数模拟为 A={}/{} B={}/{}", aMax, aMax, bMax, bMax);
         try {
             powerOffRiveting();
         } catch (Exception e) {
             log.warn("OP190 测试模式:关闭拉铆机电源跳过(无 PLC 时可忽略)", e);
         }
+        bothGunsPoweredOff = true;
         op190RivetingComplete = true;
         submitOp190QualityAsync();
     }
@@ -1142,7 +1318,7 @@ public class WorkJPanelDevice extends JPanel {
                             }
                         }else if(tjFeedingFlag == 1){
                             if(retRunning){
-                                if (oprno.equals("OP190") && !bindLbStatus) {
+                                if (oprno.equals("OP190") && !bindLbStatus && !offlineMode) {
                                     String statusText = status_menu.getText();
                                     if (statusText == null || !statusText.contains("绑定失败")
                                             && !statusText.contains("绑定请求")
@@ -1153,8 +1329,10 @@ public class WorkJPanelDevice extends JPanel {
                                     tjFeedingFlag = 2;
                                     writeAllowFeeding(false); // 工位开始工作后再清除允许上料
                                     resetFeeding();
+                                    if (oprno.equals("OP190") && offlineMode) {
+                                        initRivetingTask();
+                                    }
                                     setMenuStatus("工件加工中", 0);
-                                    getToolLife();
                                 }
                             }else{
                                 if(!retFeedingAllow){
@@ -1168,7 +1346,12 @@ public class WorkJPanelDevice extends JPanel {
                                 writeAllowBlanking(true);
                                 blankingmeslabel.setIcon(imageGreen);
 
-                                if (oprno.equals("OP190")) {
+                                if (offlineMode) {
+                                    tjFeedingFlag = 3;
+                                    resetBlanking();
+                                    setMenuStatus("已下料(离线不提交),等待下一件", 0);
+                                    log.info("{}{} 离线模式:下料完成,跳过服务端提交", oprno, pos);
+                                } else if (oprno.equals("OP190")) {
                                     // OP190:质量已在拉铆完成时异步提交;下料仅反馈。若尚未提交则兜底补交
                                     if (!op190QualitySubmitted) {
                                         final String sn = productSnFeeding.getText() == null
@@ -1233,6 +1416,15 @@ public class WorkJPanelDevice extends JPanel {
             productSnFeeding.setText(sn);
             lastSn = sn;
 
+            // 离线模式:不做工件码校验,直接允许上料
+            if (offlineMode) {
+                tjFeedingFlag = 1;
+                writeAllowFeeding(true);
+                setMenuStatus("已允许上料(不校验不提交)", 0);
+                log.info("{}{} 离线读码通过 sn={}", oprno, pos, sn);
+                return;
+            }
+
             // 查询工件质量
             JSONObject retObj = DataUtil.checkQuality(sn,oprno+pos,MesClient.user_menu.getText());
             if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
@@ -1297,6 +1489,10 @@ public class WorkJPanelDevice extends JPanel {
             setMenuStatus("请先登录", -1);
             return false;
         }
+        if (offlineMode) {
+            setMenuStatus("离线模式不提交服务端", -1);
+            return false;
+        }
         final String sn = productSnFeeding.getText() == null
                 ? "" : productSnFeeding.getText().trim();
         if (sn.isEmpty()) {
@@ -1343,26 +1539,33 @@ public class WorkJPanelDevice extends JPanel {
     }
 
     /**
-     * 初始化拉铆工位任务
-     * 下发拉铆预设数量并清空 PLC 记录
+     * 初始化双枪拉铆任务(同一 PLC:A=DB40/41,B=DB50/51)
      */
     private void initRivetingTask() {
         S7PLC rivetingPlc = getRivetingPlc();
         if (rivetingPlc == null) return;
         try {
-            // 远程开机
-            rivetingPlc.writeBoolean("DB40.0", true);
-            // 设置任务并清零
+            bothGunsPoweredOff = false;
+            // 双枪远程开机
+            rivetingPlc.writeBoolean("DB40.0.0", true);
+            rivetingPlc.writeBoolean("DB50.0.0", true);
+            // A 枪任务
             rivetingPlc.writeUInt16("DB40.2", 1);
             rivetingPlc.writeUInt16("DB40.4", aMax);
             rivetingPlc.writeInt32("DB41.32", 0);
-            
-            // 重置本地计数和 UI
-            sortA = 0;
-            aFinish = 0;
-            if (finishField != null) {
-                finishField.setText("0");
+            // B 枪任务
+            rivetingPlc.writeUInt16("DB50.2", 1);
+            rivetingPlc.writeUInt16("DB50.4", bMax);
+            rivetingPlc.writeInt32("DB51.32", 0);
+
+            resetRivetingCounters();
+            if (presetField != null) {
+                presetField.setText(String.valueOf(aMax));
+            }
+            if (presetFieldB != null) {
+                presetFieldB.setText(String.valueOf(bMax));
             }
+            log.info("OP190 双枪拉铆任务已下发 A={} B={}", aMax, bMax);
         } catch (Exception e) {
             log.error("初始化拉铆任务失败", e);
         }
@@ -1522,10 +1725,22 @@ public class WorkJPanelDevice extends JPanel {
 
     private void resetRivetingCounters() {
         sortA = 0;
+        sortB = 0;
         aFinish = 0;
+        bFinish = 0;
+        bothGunsPoweredOff = false;
         if (finishField != null) {
             finishField.setText("0");
         }
+        if (finishFieldB != null) {
+            finishFieldB.setText("0");
+        }
+        if (presetField != null) {
+            presetField.setText(String.valueOf(aMax));
+        }
+        if (presetFieldB != null) {
+            presetFieldB.setText(String.valueOf(bMax));
+        }
     }
 
     private void powerOffRiveting() {
@@ -1535,6 +1750,7 @@ public class WorkJPanelDevice extends JPanel {
         }
         try {
             rivetingPlc.writeBoolean("DB40.0.0", false);
+            rivetingPlc.writeBoolean("DB50.0.0", false);
         } catch (Exception e) {
             log.error("关闭拉铆机电源失败", e);
         }

+ 21 - 0
src/com/mes/ui/DataUtil.java

@@ -329,6 +329,27 @@ public class DataUtil {
         }
     }
 
+    /** 拉铆过程参数批量上传(与 LM 工位一致) */
+    public static JSONObject upProdParams(String upparams) {
+        try {
+            String mes_server_ip = MesClient.mes_server_ip;
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductProd/batchsave";
+            String params = "__ajax=json&oprno=" + MesClient.mes_gw
+                    + "&lineSn=" + MesClient.mes_line_sn
+                    + "&params=" + upparams;
+            log.info("upProdParams url={}, size={}", url, upparams == null ? 0 : upparams.length());
+            String result = doPost(url, params);
+            log.info("upProdParams result={}", result);
+            if (result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            return JSONObject.parseObject(result);
+        } catch (Exception e) {
+            log.error("upProdParams failed: " + e.getMessage());
+            return null;
+        }
+    }
+
     public static JSONObject saveLife(String lineSn,String oprno,String sn,String ucode,String ps) {
         try{
             String url = "http://"+MesClient.mes_server_ip+":8980/js/a/mes/mesProductCncLife/saveLife";

+ 31 - 0
src/com/mes/ui/MesClient.java

@@ -118,6 +118,7 @@ public class MesClient extends JFrame {
                     startHeartBeatTimer();
 
                     upParams();
+                    upProds();
                     startSubmitRetryTimer();
 
 
@@ -229,6 +230,36 @@ public class MesClient extends JFrame {
         }, 1000,120*1000);
     }
 
+    public static Timer cjTimerProd;
+
+    /** 定时上传 OP190 拉铆过程参数到 mesProductProd */
+    public static void upProds() {
+        if (cjTimerProd != null) {
+            cjTimerProd.cancel();
+        }
+        cjTimerProd = new Timer();
+        cjTimerProd.schedule(new TimerTask() {
+            public void run() {
+                try {
+                    List<ProdReq> prods = JdbcUtils.getProds();
+                    if (prods.isEmpty()) {
+                        return;
+                    }
+                    JSONObject retObj = DataUtil.upProdParams(JSON.toJSONString(prods));
+                    if (retObj != null && retObj.get("result") != null
+                            && retObj.get("result").toString().equalsIgnoreCase("true")) {
+                        for (ProdReq prodReq : prods) {
+                            JdbcUtils.updateBwProdSync(prodReq.getId(), 1);
+                        }
+                        log.info("拉铆过程参数上传成功 count={}", prods.size());
+                    }
+                } catch (Exception e) {
+                    log.error("upProds failed: " + e.getMessage());
+                }
+            }
+        }, 1000, 30 * 1000);
+    }
+
     //读配置文件
     private static void readProperty() throws IOException{
         String enconding = "UTF-8";

+ 148 - 0
src/com/mes/ui/ProdReq.java

@@ -0,0 +1,148 @@
+package com.mes.ui;
+
+public class ProdReq {
+    private Integer id;
+    private String gw;
+    private String lineSn;
+    private String type;
+    private String sn;
+    private String fout;
+    private String sout;
+    private String fmin;
+    private String smin;
+    private String fmax;
+    private String smax;
+    private String qty;
+    private String serialNumber;
+    private Integer sync;
+    private String recordTime;
+    private String ucode;
+
+    public String getUcode() {
+        return ucode;
+    }
+
+    public void setUcode(String ucode) {
+        this.ucode = ucode;
+    }
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getGw() {
+        return gw;
+    }
+
+    public void setGw(String gw) {
+        this.gw = gw;
+    }
+
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public void setLineSn(String lineSn) {
+        this.lineSn = lineSn;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    public String getFout() {
+        return fout;
+    }
+
+    public void setFout(String fout) {
+        this.fout = fout;
+    }
+
+    public String getSout() {
+        return sout;
+    }
+
+    public void setSout(String sout) {
+        this.sout = sout;
+    }
+
+    public String getFmin() {
+        return fmin;
+    }
+
+    public void setFmin(String fmin) {
+        this.fmin = fmin;
+    }
+
+    public String getSmin() {
+        return smin;
+    }
+
+    public void setSmin(String smin) {
+        this.smin = smin;
+    }
+
+    public String getFmax() {
+        return fmax;
+    }
+
+    public void setFmax(String fmax) {
+        this.fmax = fmax;
+    }
+
+    public String getSmax() {
+        return smax;
+    }
+
+    public void setSmax(String smax) {
+        this.smax = smax;
+    }
+
+    public String getQty() {
+        return qty;
+    }
+
+    public void setQty(String qty) {
+        this.qty = qty;
+    }
+
+    public String getSerialNumber() {
+        return serialNumber;
+    }
+
+    public void setSerialNumber(String serialNumber) {
+        this.serialNumber = serialNumber;
+    }
+
+    public Integer getSync() {
+        return sync;
+    }
+
+    public void setSync(Integer sync) {
+        this.sync = sync;
+    }
+
+    public String getRecordTime() {
+        return recordTime;
+    }
+
+    public void setRecordTime(String recordTime) {
+        this.recordTime = recordTime;
+    }
+}

+ 83 - 0
src/com/mes/util/JdbcUtils.java

@@ -1,6 +1,7 @@
 package com.mes.util;
 
 import com.mes.ui.ParamReq;
+import com.mes.ui.ProdReq;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -84,6 +85,16 @@ public class JdbcUtils {
 				+ ")";
 		statement.executeUpdate(sqlEquipment2);
 
+		// 拉铆过程参数表(与 LM / OP260 一致,上传 mesProductProd)
+		String sqlProd = "CREATE TABLE if not exists bw_prod("
+				+ "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+				+ "gw VARCHAR(20),line_sn VARCHAR(20),type VARCHAR(10),sn VARCHAR(50),"
+				+ "fout VARCHAR(50),sout VARCHAR(50),fmin VARCHAR(50),smin VARCHAR(50),"
+				+ "fmax VARCHAR(50),smax VARCHAR(50),qty VARCHAR(10),serial_number VARCHAR(20),"
+				+ "ucode VARCHAR(50),record_time DATETIME,sync int(10) NULL DEFAULT 0"
+				+ ")";
+		statement.executeUpdate(sqlProd);
+
 		// 创建 提交记录表
 		String cache = "CREATE TABLE if not exists cache(\n" +
 				"   id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增ID\n" +
@@ -131,6 +142,78 @@ public class JdbcUtils {
 		statement.close();
 	}
 
+	/** 插入拉铆单颗过程数据,待定时上传 mesProductProd */
+	public static boolean insertProdData(String gw, String lineSn, String sn, String type,
+										 String fout, String sout, String fmin, String smin,
+										 String fmax, String smax, String qty, String serialNumber, String ucode) {
+		if (sn == null || sn.trim().isEmpty()) {
+			return false;
+		}
+		JdbcUtils.getConn();
+		boolean ret = false;
+		String record_time = DateLocalUtils.getCurrentTime();
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("INSERT INTO bw_prod (gw,line_sn,type,sn,fout,sout,fmin,smin,fmax,smax,qty,serial_number,ucode,record_time) VALUES"
+					+ " ('" + gw + "', '" + lineSn + "', '" + type + "', '" + sn + "', '" + fout + "', '" + sout
+					+ "', '" + fmin + "', '" + smin + "', '" + fmax + "', '" + smax + "', '" + qty
+					+ "','" + serialNumber + "','" + ucode + "','" + record_time + "')");
+			statement.close();
+			ret = true;
+		} catch (SQLException e) {
+			log.error("insertProdData failed: " + e.getMessage());
+			ret = false;
+		}
+		return ret;
+	}
+
+	public static void updateBwProdSync(Integer id, Integer sync) throws SQLException {
+		Connection conn = JdbcUtils.getConn();
+		Statement statement = conn.createStatement();
+		statement.executeUpdate("update bw_prod set sync = " + sync + " where id = " + id);
+		statement.close();
+	}
+
+	public static List<ProdReq> getProds() {
+		String sql = "select id,gw,line_sn,type,sn,fout,sout,fmin,smin,fmax,smax,qty,serial_number,sync,record_time,ucode from bw_prod where sync = 0 order by id asc limit 100";
+		Connection conn = JdbcUtils.getConn();
+		Statement stmt = null;
+		ResultSet ret = null;
+		List<ProdReq> prods = new ArrayList<>();
+		try {
+			stmt = conn.createStatement();
+			ret = stmt.executeQuery(sql);
+			while (ret.next()) {
+				ProdReq prodReq = new ProdReq();
+				prodReq.setId(ret.getInt(1));
+				prodReq.setGw(ret.getString(2));
+				prodReq.setLineSn(ret.getString(3));
+				prodReq.setType(ret.getString(4));
+				prodReq.setSn(ret.getString(5));
+				prodReq.setFout(ret.getString(6));
+				prodReq.setSout(ret.getString(7));
+				prodReq.setFmin(ret.getString(8));
+				prodReq.setSmin(ret.getString(9));
+				prodReq.setFmax(ret.getString(10));
+				prodReq.setSmax(ret.getString(11));
+				prodReq.setQty(ret.getString(12));
+				prodReq.setSerialNumber(ret.getString(13));
+				prodReq.setSync(ret.getInt(14));
+				prodReq.setRecordTime(ret.getString(15));
+				prodReq.setUcode(ret.getString(16));
+				prods.add(prodReq);
+			}
+			ret.close();
+			stmt.close();
+		} catch (SQLException e1) {
+			e1.printStackTrace();
+		}
+		return prods;
+	}
+
 	public static String getCache(String key) {
 		//SQL语句
 		String sql = "select id,sn,content from cache where sn = '"+key+"' order by id asc limit 1";

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

@@ -1,5 +1,5 @@
 #mes.server_ip=127.0.0.1
 mes.server_ip=192.168.15.99
 mes.line_sn=XT
-mes.gw=OP140
+mes.gw=OP190
 mes.line_pos=west