dkk 2 дней назад
Родитель
Сommit
ea0aaa6063

+ 1 - 0
.classpath

@@ -21,4 +21,5 @@
 	<classpathentry kind="lib" path="lib/sqlite-jdbc-3.36.0.3.jar"/>
 	<classpathentry kind="lib" path="lib/commons-codec-1.15.jar"/>
 	<classpathentry kind="lib" path="lib/fastjson2-2.0.16.jar"/>
+	<classpathentry kind="lib" path="lib/jSerialComm-2.10.4.jar"/>
 </classpath>

+ 11 - 0
.project

@@ -14,4 +14,15 @@
 	<natures>
 		<nature>org.eclipse.jdt.core.javanature</nature>
 	</natures>
+	<filteredResources>
+		<filter>
+			<id>1785201741936</id>
+			<name></name>
+			<type>30</type>
+			<matcher>
+				<id>org.eclipse.core.resources.regexFilterMatcher</id>
+				<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
 </projectDescription>

BIN
lib/jSerialComm-2.10.4.jar


+ 62 - 155
src/com/mes/ui/MesClient.java

@@ -56,7 +56,7 @@ public class MesClient extends JFrame {
     public static JScrollPane indexScrollPaneA;
     public static JScrollPane searchScrollPane;
     public static JScrollPane searchScrollPaneDj;
-    public static SerialPortUtils serialPortUtils;
+    public static WeightSerialService serialPortService;
     public static  ParamConfig paramConfig;
     public static Boolean check_quality_result = false;
     public static Integer work_status = 0;
@@ -67,13 +67,10 @@ public class MesClient extends JFrame {
 
     public static int scan_type = 0;
     public static JButton finish_ok_bt;
-    public static JButton batch_sn_bt;
     public static JTextField product_sn;
     public static JButton f_scan_data_bt_1;
-    public static JButton is_js_btn;
     public static String comboBoxValue = "COM1";
     public static String user20 = "";
-    public static String is_js_btn_val = "1";
 
     public static JFrame welcomeWin;
 
@@ -116,14 +113,54 @@ public class MesClient extends JFrame {
     }
 
     public static void initComPort() {
-        serialPortUtils = new SerialPortUtils();
-         paramConfig = new ParamConfig((String) comboBox.getSelectedItem(), 1200, 0, 8, 1);
-//         paramConfig = new ParamConfig("COM7", 1200, 0, 8, 1);
-        System.out.println("串口号:"+(String) comboBox.getSelectedItem());
-        //初始化串口
-        serialPortUtils.init(paramConfig);
-        System.out.println("初始化com串口结束...");
+        // 若已有旧实例,先关闭
+        if (serialPortService != null) {
+            serialPortService.close();
+            serialPortService = null;
+        }
+        serialPortService = new WeightSerialService();
+        // 默认波特率 9600(可通过 ParamConfig.DEFAULT_BAUD_RATE 修改)
+        paramConfig = new ParamConfig((String) comboBox.getSelectedItem(), ParamConfig.DEFAULT_BAUD_RATE);
+        System.out.println("串口号:" + comboBox.getSelectedItem() + " 波特率:" + paramConfig.getBaudRate());
+
+        // 注册重量数据监听器:收到数据时同步连接状态并更新 UI
+        serialPortService.addListener(new WeightSerialService.WeightDataListener() {
+            @Override
+            public void onWeightData(WeightData data) {
+                // 仅在"称重"状态下更新界面显示
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (weight_text != null) {
+                            // 秤端发来的小数点位数比实际多1,导致值被多除以10,此处乘以10修正
+                            weight_text.setText(String.valueOf(data.getValue() * 10));
+                        }
+                        System.out.println("收到重量数据: " + data);
+                    }
+                });
+            }
 
+            @Override
+            public void onConnectionChanged(boolean connected) {
+                com_connect_flag = connected;
+                System.out.println("串口连接状态: " + (connected ? "已连接" : "已断开"));
+            }
+
+            @Override
+            public void onError(Throwable t) {
+                System.err.println("串口通信异常: " + t.getMessage());
+                t.printStackTrace();
+            }
+        });
+
+        boolean ok = serialPortService.open(paramConfig);
+        if (ok) {
+            com_connect_flag = true;
+            System.out.println("com串口连接成功...");
+        } else {
+            com_connect_flag = false;
+            System.out.println("com串口连接失败...");
+        }
     }
 
     //定时任务重连吊秤
@@ -135,7 +172,7 @@ public class MesClient extends JFrame {
             MesClient.cjTimer2.schedule(new TimerTask() {
                 public void run() {
                     try {
-                        if (!com_connect_flag) {
+                        if (serialPortService == null || !serialPortService.isConnected()) {
                             System.out.println("吊秤断开连接,正在重连");
                             initComPort();
                         }
@@ -192,9 +229,7 @@ public class MesClient extends JFrame {
     public static java.util.Timer heartBeatIconTimer;
     public static boolean iconREDFlag = true;
     public static JTextField weight_text;
-    public static JTextField batch_sn_text;
     public static JLabel lblNewLabel;
-    public static JLabel lblNewLabel_1;
     private static JComboBox comboBox;
     public static JButton finish_ok_bt_1;
     public static void startHeartBeatTimer() {
@@ -273,30 +308,15 @@ public class MesClient extends JFrame {
     public static void resetScanA() {
         work_status = 0;
         check_quality_result = false;
-//		MesClient.start_work_bt.setVisible(false);
-//        MesClient.finish_ng_bt.setVisible(false);
-//        MesClient.finish_ok_bt.setVisible(false);
         MesClient.finish_ok_bt.setEnabled(false);
         product_sn.setText("");
-        batch_sn_text.setText("");
         weight_text.setText("");
-        batch_sn_bt.setEnabled(false);
         weight_text.setEnabled(false);
-        is_js_btn.setText("最后一台");
-        is_js_btn_val = "1";
-        is_js_btn.setBackground(null);
         finish_ok_bt.setEnabled(false);
         MesClient.finish_ok_bt_1.setText("称重");
-//        MesClient.f_scan_data_bt_1.setEnabled(false);
 
         MesClient.f_scan_data_bt_1.setEnabled(true);
-//		DataUtil.stopWork(sessionid);
         setMenuStatus("请扫工件码",0);
-//		product_result_text.setText("");
-//		work_status_text.setText("");
-//        MesClient.setMenuStatus("涂胶已超过10分钟,已失效",-1);
-
-//        updateMaterailData();
         shiftUserCheck();
     }
 
@@ -556,7 +576,9 @@ public class MesClient extends JFrame {
             @Override
             public void actionPerformed(ActionEvent e) {
                 //修改串口时,先关闭资源,重新初始化
-                serialPortUtils.closeSerialPort();
+                if (serialPortService != null) {
+                    serialPortService.close();
+                }
                 MesClient.initComPort();
             }
         });
@@ -597,48 +619,21 @@ public class MesClient extends JFrame {
 //        mesRadioHj.setBounds(190,170,500,50);
 //        indexPanelA.add(mesRadioHj);
 
-        finish_ok_bt = new JButton("打包");
+        finish_ok_bt = new JButton("OK");
         finish_ok_bt.setEnabled(false);
         finish_ok_bt.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent e) {
                 if(work_status == 1 && check_quality_result) {
-
-                    if ( batch_sn_text.getText().equals("")){
-                        JOptionPane.showMessageDialog(mesClientFrame,"请先获取包装码!","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                    // 调用bindWarehouse绑定重量并上传:wsn=重量, sn=工件码, craft=400006
+                    String sn = getBarcode(product_sn.getText());
+                    getUser();
+                    String wsn = weight_text.getText();
+                    Boolean sendret = DataUtil.bindWarehouse(nettyClient, sn, wsn, user20, "400006");
+                    if (!sendret) {
+                        JOptionPane.showMessageDialog(mesClientFrame, "消息发送失败,请重试", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                        return;
                     }
-                    String is_js = is_js_btn_val;
-                    String result = HttpUtils.sendRequest(
-                            "http://"+MesClient.mes_server_ip+":8980/js/a/mes/mesProductRecord/dp350?" +
-                                    "sn=" + product_sn.getText() + "&oprno=" + mes_gw + "&batchSn=" +
-                                    batch_sn_text.getText() + "&weight=" + weight_text.getText() + "&isJs=" + is_js);
-//                                    batch_sn_text.getText() + "&weight=123" + "&isJs=" + is_js);
-//                    String result = HttpUtils.sendRequest(
-//                            "http://127.0.0.1:8980/js/a/mes/mesProductRecord/dp350?" +
-//                                    "sn=" + product_sn.getText() + "&oprno=" + mes_gw + "&batchSn=" +
-//                                    batch_sn_text.getText() + "&weight=" + weight_text.getText() + "&isJs=" + is_js);
-                    if (result != null && !result.equals("") && !result.equals("false")) {
-                        JSONObject ret = JSON.parseObject(result);
-                        if (ret.get("result").equals("true")) {
-                            MesClient.setMenuStatus("结果提交成功,请扫下一件",0);
-                            resetScanA();
-//                            cjTimer.cancel();
-//                            MesClient.weight_text.setText("");
-                        }else {
-                            JOptionPane.showMessageDialog(mesClientFrame, "结果提交失败,"+ret.get("message")+"!", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
-                            MesClient.setMenuStatus("结果提交失败,请重试!",1);
-                        }
-                    }
-
-//                    String sn = getBarcode(product_sn.getText());
-//                    getUser();
-//
-//                    String qret = "OK";
-//                    Boolean sendret = DataUtil.sendQuality(nettyClient, sn, qret, user20);
-//                    if (!sendret) {
-//                        JOptionPane.showMessageDialog(mesClientFrame, "消息发送失败,请重试", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
-//                        return;
-//                    }
-
+                    MesClient.setMenuStatus("正在上传结果...", 0);
                 }
             }
         });
@@ -655,65 +650,6 @@ public class MesClient extends JFrame {
         weight_text.setBounds(403, 300, 181, 70);
         indexPanelA.add(weight_text);
         
-        batch_sn_text = new JTextField();
-        batch_sn_text.setEditable(false);
-        batch_sn_text.setHorizontalAlignment(SwingConstants.CENTER);
-        batch_sn_text.setFont(new Font("微软雅黑", Font.PLAIN, 28));
-        batch_sn_text.setColumns(10);
-        batch_sn_text.setBounds(403, 192, 280, 70);
-        indexPanelA.add(batch_sn_text);
-
-        batch_sn_bt = new JButton("生成包装码");
-        batch_sn_bt.setEnabled(false);
-        batch_sn_bt.addActionListener(new ActionListener() {
-        	public void actionPerformed(ActionEvent e) {
-                //生成包装码
-                try {
-                    String result = HttpUtils.sendRequest("http://"+MesClient.mes_server_ip+":8980/js/a/mes/mesBatchSn/createBatchSn?lineSn="+mes_line_sn);
-                    JSONObject ret = JSON.parseObject(result);
-                    String TF = ret.getString("result");
-                    if (Boolean.parseBoolean(TF)){
-                        String batchSn = ret.getJSONObject("data").getString("batchSn");
-                        //赋值  禁用生成按钮
-                        batch_sn_text.setText(batchSn);
-                        MesClient.batch_sn_bt.setEnabled(false);
-                        MesClient.finish_ok_bt.setEnabled(true);
-                        setMenuStatus("生成包装码成功",0);
-                    }else{
-                        JOptionPane.showMessageDialog(mesClientFrame,"生成包装码失败!,请重试","提示窗口", JOptionPane.INFORMATION_MESSAGE);
-
-                    }
-                } catch (Exception ex) {
-                    ex.printStackTrace();
-                    JOptionPane.showMessageDialog(mesClientFrame,"生成包装码失败!,请重试","提示窗口", JOptionPane.INFORMATION_MESSAGE);
-
-                }
-
-//                if (result!= null&&!result.equals("")&&!result.equals("false")) {
-//                    JSONObject ret = JSON.parseObject(result);
-//                    if (ret != null && ret.containsKey("data")) {
-//                        String batchSn = ret.getJSONObject("data").getString("batchSn");
-//                        if (batchSn == null || batchSn.equals("")) { // 包装码为null,激活生成包装码按钮
-//                            JOptionPane.showMessageDialog(mesClientFrame,"生成包装码失败!,请重试","提示窗口", JOptionPane.INFORMATION_MESSAGE);
-//                        }else{
-//                            //赋值  禁用生成按钮
-//                            batch_sn_text.setText(batchSn);
-//                            MesClient.batch_sn_bt.setEnabled(false);
-//                        }
-//                    }
-//
-//                }else{
-//                    JOptionPane.showMessageDialog(mesClientFrame,"生成包装码失败!,请重试","提示窗口", JOptionPane.INFORMATION_MESSAGE);
-//                }
-
-
-
-        	}
-        });
-        batch_sn_bt.setFont(new Font("微软雅黑", Font.PLAIN, 30));
-        batch_sn_bt.setBounds(693, 192, 198, 70);
-        indexPanelA.add(batch_sn_bt);
-        
         lblNewLabel = new JLabel("重量");
         lblNewLabel.setEnabled(false);
         lblNewLabel.setHorizontalAlignment(SwingConstants.RIGHT);
@@ -721,35 +657,6 @@ public class MesClient extends JFrame {
         lblNewLabel.setBounds(238, 300, 117, 70);
         indexPanelA.add(lblNewLabel);
         
-        lblNewLabel_1 = new JLabel("包装码");
-        lblNewLabel_1.setEnabled(false);
-        lblNewLabel_1.setHorizontalAlignment(SwingConstants.RIGHT);
-        lblNewLabel_1.setFont(new Font("微软雅黑", Font.PLAIN, 30));
-        lblNewLabel_1.setBounds(238, 192, 117, 70);
-        indexPanelA.add(lblNewLabel_1);
-        
-        is_js_btn = new JButton("最后一台");
-        is_js_btn.addActionListener(new ActionListener() {
-        	public void actionPerformed(ActionEvent e) {
-//                if (is_js_btn.getText().equals("结束")){
-//                    is_js_btn.setText("开始");
-//                }else if (is_js_btn.getText().equals("开始")){
-//                    is_js_btn.setText("结束");
-//                }
-                if (is_js_btn_val.equals("2")){
-                    is_js_btn.setBackground(null);
-                    is_js_btn_val = "1";
-                }else{
-//                    is_js_btn.setText("结束");
-                    is_js_btn.setBackground(Color.GREEN);
-                    is_js_btn_val = "2";
-                }
-        	}
-        });
-        is_js_btn.setFont(new Font("微软雅黑", Font.PLAIN, 32));
-        is_js_btn.setBounds(693, 300, 198, 70);
-        indexPanelA.add(is_js_btn);
-        
         finish_ok_bt_1 = new JButton("确定");
         finish_ok_bt_1.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
         finish_ok_bt_1.addActionListener(new ActionListener() {

+ 8 - 41
src/com/mes/ui/MesRevice.java

@@ -60,45 +60,9 @@ public class MesRevice {
                 MesClient.work_status = 1;
 
                 MesClient.f_scan_data_bt_1.setEnabled(false);
-
-//                String sn = MesClient.getBarcode(MesClient.product_sn.getText());
-//                MesClient.getUser();
-//                DataUtil.bindUser(MesClient.nettyClient,sn,MesClient.user20);
-
-                try {
-                    String result = HttpUtils.sendRequest("http://"+MesClient.mes_server_ip+":8980/js/a/mes/mesBatchSn/getBatchSn?lineSn="+MesClient.mes_line_sn);
-
-                    JSONObject ret = JSON.parseObject(result);
-                    String TF = ret.getString("result");
-                    if (Boolean.parseBoolean(TF)){
-                        String batchSn = ret.getJSONObject("data").getString("batchSn");
-                        if(!batchSn.isEmpty()){
-                            MesClient.batch_sn_bt.setEnabled(false);
-                            MesClient.batch_sn_text.setText(batchSn);
-                            MesClient.finish_ok_bt.setEnabled(true);
-                            MesClient.setMenuStatus("获取包装码成功",0);
-                        }else{
-                            MesClient.batch_sn_bt.setEnabled(true);
-                        }
-                    }else{
-                        MesClient.batch_sn_bt.setEnabled(true);
-                    }
-                } catch (Exception e) {
-                    e.printStackTrace();
-                    MesClient.batch_sn_bt.setEnabled(true);
-                }
-
-//                MesClient.finish_ng_bt.setEnabled(true);
-
-                /*String qret = "OK";
-                Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,sn,qret,MesClient.user20);
-                if(!sendret){
-//                    MesClient.finish_ok_bt.setEnabled(true);
-//                    MesClient.finish_ng_bt.setEnabled(true);
-                    MesClient.status_menu.setText("结果提交失败,请重试");
-                    JOptionPane.showMessageDialog(MesClient.mesClientFrame,"结果提交失败,请重试","提示窗口", JOptionPane.INFORMATION_MESSAGE);
-                    return;
-                }*/
+                // 质检通过且开工成功,OK按钮亮起
+                MesClient.finish_ok_bt.setEnabled(true);
+                MesClient.setMenuStatus("可以上传结果",0);
             }
         }catch (Exception e){
             e.printStackTrace();
@@ -109,9 +73,12 @@ public class MesRevice {
     public static void bindRevice(String processMsgRet,String mes_msg){
         try{
             if(processMsgRet.equalsIgnoreCase("OK")) {
-
+                MesClient.resetScanA();
+                MesClient.setMenuStatus("结果提交成功,请扫下一件",0);
+                MesClient.scan_type = 1;
+                MesClient.scanBarcode();
             }else{
-
+                MesClient.setMenuStatus("结果提交失败,请重试",-1);
             }
         }catch (Exception e){
             e.printStackTrace();

+ 20 - 10
src/com/mes/ui/OprnoUtil.java

@@ -6,18 +6,28 @@ import java.util.Map;
 
 public class OprnoUtil {
     public static String[] xtoprnos = new String[]{
-            "OP040","OP050","OP060","OP070","OP080","OP090","OP100","OP110",
-            "OP120","OP130","OP140","OP150","OP160","OP170","OP180","OP190",
-            "OP200","OP210","OP220","OP230","OP240","OP250","OP260","OP270",
-            "OP280","OP290","OP300","OP310","OP320","OP330","OP340","OP350",
-            "OP360","OP370","OP380","OP390","OP400","OP410","OP420","OP430"
+            "OP040","OP050","OP060","OP070","OP080","OP090", "OP100","OP110A","OP110B","OP110C",
+            "OP120","OP130","OP140","OP150","OP160", "OP170A","OP170B","OP170C","OP170D",
+            "OP180", "OP190A","OP190B","OP190C", "OP200", "OP210A","OP210B", "OP210C","OP220",
+            "OP230", "OP240A","OP240B", "OP250", "OP260", "OP270", "OP275", "OP280A","OP280B",
+            "OP290", "OP300", "OP305", "OP310", "OP315", "OP320", "OP330A","OP330B", "OP340A",
+            "OP340B", "OP350", "OP360", "OP370", "OP380", "OP390", "OP400", "OP410", "OP420",
+            "OP430", "OP440", "OP450", "OP460"
     };
+
     public static String[] xtoprnodes = new String[]{
-            "镭雕二维码","小总成焊接(边梁、后梁)","框架焊接","人工补焊","框架矫形","焊道检验","CNC总成反面(1序)","总成反面清洁",
-            "框架找漏","冷板安装+拉铆","清理溢胶","水冷板FSW","CNC总成反面(2序)","CNC总成正面加工","总成正面清洁","框架终检",
-            "恒温静置","正面安装钢丝螺套","反面安装钢丝螺套","反面拉铆","半成品气密","涂液冷板加强件结构胶","涂密封胶 安装加强件","清胶",
-            "胶水固化","底护板预装+水嘴安装","底护板装配","成品气密","液冷板气密","反面贴EPDM保温棉","贴PI膜/限位条","正面涂胶+拉铆",
-            "安装复合横梁","正面贴EPDM保温棉","绝缘耐压测试","在线检测(错漏)","在线检测(尺寸)","终检","GP12","包装"
+            "单部件压铆+涂胶", "压铆", "单部件装配", "左边梁镭雕", "CMT框架焊接", "人工补焊",
+            "焊缝打磨", "框架反面CNC(1序)", "框架反面CNC(2序)", "框架反面CNC(3序)", "框架去毛刺+清洁+牙套",
+            "液冷板拉铆", "液冷板点焊+水嘴焊接", "液冷板FSW", "半总成找漏", "总成反面CNC(1序)",
+            "总成反面CNC(2序)", "总成反面CNC(3序)", "总成反面CNC(4序)", "匙孔补焊打磨+搅拌焊道打磨",
+            "总成正面CNC(1序)", "总成正面CNC(2序)", "总成正面CNC(3序)", "总成清洁", "总成反面装配1(1序)",
+            "总成反面装配1(2序)", "总成反面装配1(3序)", "总成反面装配2", "总成正面装配1",
+            "总成正面装配2(1序)", "总成正面装配2(2序)", "中吊点安装", "边梁套筒涂胶+压合",
+            "箱体封堵", "胶水固化+静置", "半成品气密(1序)", "半成品气密(2序)", "密封件装配+加强板安装",
+            "双层拉铆螺母涂胶", "胶水固化", "焊道涂胶", "胶水固化", "加强板压紧工装拆卸+泡棉粘贴", "底护板装配(1序)",
+            "底护板装配(2序)", "总成气密(1序)", "总成气密(2序)", "底护板螺栓复拧", "液冷板气密", "反面终检",
+            "PC条+陶瓷复合带粘贴+水嘴封堵", "总成清洁+PET粘贴", "CCD", "总成检具检验", "模拟客户安装界面(装拆)",
+            "正面终检", "正面GP12", "反面GP12", "称重"
     };
     public static String[] lboprnos = new String[]{
             "OP040","OP050","OP060","OP070","OP080","OP090","OP100","OP110",

+ 40 - 1
src/com/mes/util/ParamConfig.java

@@ -1,13 +1,52 @@
 package com.mes.util;
 
+/**
+ * 串口参数配置
+ * 用于封装串口通信所需的全部参数:串口号、波特率、数据位、停止位、校验位。
+ * 可通过 {@link #DEFAULT_BAUD_RATE} 修改默认波特率。
+ */
 public class ParamConfig {
+
+    /** 默认波特率(可修改) */
+    public static final int DEFAULT_BAUD_RATE = 9600;
+    /** 默认数据位 */
+    public static final int DEFAULT_DATA_BIT = 8;
+    /** 默认停止位 */
+    public static final int DEFAULT_STOP_BIT = 1;
+    /** 默认校验位(0=NONE) */
+    public static final int DEFAULT_CHECKOUT_BIT = 0;
+
     private String serialNumber;// 串口号
     private int baudRate;       // 波特率
     private int checkoutBit;    // 校验位
     private int dataBit;        // 数据位
     private int stopBit;        // 停止位
 
-    public ParamConfig() {}
+    public ParamConfig() {
+        this.baudRate = DEFAULT_BAUD_RATE;
+        this.dataBit = DEFAULT_DATA_BIT;
+        this.stopBit = DEFAULT_STOP_BIT;
+        this.checkoutBit = DEFAULT_CHECKOUT_BIT;
+    }
+
+    /**
+     * 构造方法:仅指定串口号,其余使用默认参数(9600, 8, 1, NONE)
+     *
+     * @param serialNumber 串口号,例如 "COM3"
+     */
+    public ParamConfig(String serialNumber) {
+        this(serialNumber, DEFAULT_BAUD_RATE, DEFAULT_CHECKOUT_BIT, DEFAULT_DATA_BIT, DEFAULT_STOP_BIT);
+    }
+
+    /**
+     * 构造方法:指定串口号和波特率,其余使用默认参数(8, 1, NONE)
+     *
+     * @param serialNumber 串口号
+     * @param baudRate     波特率
+     */
+    public ParamConfig(String serialNumber, int baudRate) {
+        this(serialNumber, baudRate, DEFAULT_CHECKOUT_BIT, DEFAULT_DATA_BIT, DEFAULT_STOP_BIT);
+    }
 
     /**
      * 构造方法

+ 164 - 0
src/com/mes/util/WeightData.java

@@ -0,0 +1,164 @@
+package com.mes.util;
+
+/**
+ * 称重数据解析结果模型
+ * 封装从串口数据帧解析出的所有重量信息,供上层MES系统调用。
+ */
+public class WeightData {
+
+    /** 工作模式:计重 */
+    public static final int MODE_WEIGHT = 0;
+    /** 工作模式:计数 */
+    public static final int MODE_COUNT = 1;
+    /** 工作模式:百分比 */
+    public static final int MODE_PERCENT = 2;
+
+    /** 单位:公斤 */
+    public static final String UNIT_KG = "kg";
+    /** 单位:磅 */
+    public static final String UNIT_LB = "lb";
+
+    /** 原始数据帧的十六进制表示 */
+    private String rawHex;
+
+    /** 显示数值(已根据小数点位数换算后的实际重量值,带符号) */
+    private double value;
+
+    /** BCD 解析出的原始整数显示值(未应用小数点) */
+    private long rawDisplayValue;
+
+    /** 小数点位数(0~5) */
+    private int decimalPlaces;
+
+    /** 工作模式(0=计重, 1=计数, 2=百分比) */
+    private int workMode;
+
+    /** 是否为负重量(true=负, false=正) */
+    private boolean negative;
+
+    /** 重量是否稳定(true=稳定, false=不稳定) */
+    private boolean stable;
+
+    /** 重量是否溢出(true=溢出, false=正常) */
+    private boolean overflow;
+
+    /** 单位字符串("kg" 或 "lb") */
+    private String unit;
+
+    /** 接收到该帧的时间戳 */
+    private long timestamp;
+
+    public WeightData() {
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    /** 获取带单位字符串的重量值,例如 "-12.34 kg" */
+    public String getFormattedValue() {
+        return String.valueOf(value) + " " + unit;
+    }
+
+    public String getRawHex() {
+        return rawHex;
+    }
+
+    public void setRawHex(String rawHex) {
+        this.rawHex = rawHex;
+    }
+
+    public double getValue() {
+        return value;
+    }
+
+    public void setValue(double value) {
+        this.value = value;
+    }
+
+    public long getRawDisplayValue() {
+        return rawDisplayValue;
+    }
+
+    public void setRawDisplayValue(long rawDisplayValue) {
+        this.rawDisplayValue = rawDisplayValue;
+    }
+
+    public int getDecimalPlaces() {
+        return decimalPlaces;
+    }
+
+    public void setDecimalPlaces(int decimalPlaces) {
+        this.decimalPlaces = decimalPlaces;
+    }
+
+    public int getWorkMode() {
+        return workMode;
+    }
+
+    public void setWorkMode(int workMode) {
+        this.workMode = workMode;
+    }
+
+    public boolean isNegative() {
+        return negative;
+    }
+
+    public void setNegative(boolean negative) {
+        this.negative = negative;
+    }
+
+    public boolean isStable() {
+        return stable;
+    }
+
+    public void setStable(boolean stable) {
+        this.stable = stable;
+    }
+
+    public boolean isOverflow() {
+        return overflow;
+    }
+
+    public void setOverflow(boolean overflow) {
+        this.overflow = overflow;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    /** 获取工作模式的中文名称 */
+    public String getWorkModeName() {
+        switch (workMode) {
+            case MODE_WEIGHT: return "计重";
+            case MODE_COUNT: return "计数";
+            case MODE_PERCENT: return "百分比";
+            default: return "未知";
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "WeightData{" +
+                "rawHex='" + rawHex + '\'' +
+                ", value=" + value +
+                ", unit='" + unit + '\'' +
+                ", decimalPlaces=" + decimalPlaces +
+                ", workMode=" + getWorkModeName() +
+                ", negative=" + negative +
+                ", stable=" + stable +
+                ", overflow=" + overflow +
+                ", timestamp=" + timestamp +
+                '}';
+    }
+}

+ 188 - 0
src/com/mes/util/WeightProtocolParser.java

@@ -0,0 +1,188 @@
+package com.mes.util;
+
+/**
+ * 称重仪表协议解析器
+ *
+ * 协议说明:每帧 6 字节
+ *   字节1: 0xFF 帧起始标志
+ *   字节2: 状态字节
+ *       D0~D2: 小数点位数 (0~5)
+ *       D3~D4: 工作模式 (00=计重, 01=计数, 10=百分比)
+ *       D5   : 重量符号 (1=负, 0=正)
+ *       D6   : 重量稳定状态 (1=稳定, 0=不稳定)
+ *       D7   : 重量溢出状态 (1=溢出, 0=正常)
+ *   字节3: BCD1 显示数值最低字节
+ *   字节4: BCD2 显示数值中间字节
+ *   字节5: BCD3 显示数值最高字节
+ *   字节6: 单位 (1=磅, 0=公斤)
+ *
+ * BCD 编码:每个字节高 4 位为十位、低 4 位为个位。
+ * 组合顺序(从高到低):BCD3 -> BCD2 -> BCD1,共 6 位十进制数字。
+ */
+public class WeightProtocolParser {
+
+    /** 帧长度 */
+    public static final int FRAME_LENGTH = 6;
+
+    /** 帧头标志字节 */
+    public static final byte FRAME_HEADER = (byte) 0xFF;
+
+    private WeightProtocolParser() {
+    }
+
+    /**
+     * 将 byte 数组转换为十六进制字符串(大写,每字节两位,无分隔符)
+     */
+    public static String bytesToHexString(byte[] bytes, int offset, int length) {
+        if (bytes == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder(length * 2);
+        for (int i = offset; i < offset + length && i < bytes.length; i++) {
+            String sTemp = Integer.toHexString(0xFF & bytes[i]);
+            if (sTemp.length() < 2) {
+                sb.append('0');
+            }
+            sb.append(sTemp.toUpperCase());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 将 byte 数组转换为十六进制字符串(大写)
+     */
+    public static String bytesToHexString(byte[] bytes) {
+        return bytes == null ? "" : bytesToHexString(bytes, 0, bytes.length);
+    }
+
+    /**
+     * 判断第一字节是否为帧头 0xFF
+     */
+    public static boolean isFrameHeader(byte b) {
+        return (b & 0xFF) == 0xFF;
+    }
+
+    /**
+     * 校验数据帧完整性
+     *
+     * @param frame 6 字节数据帧
+     * @return true 表示帧合法
+     */
+    public static boolean validateFrame(byte[] frame) {
+        if (frame == null || frame.length != FRAME_LENGTH) {
+            return false;
+        }
+        if (!isFrameHeader(frame[0])) {
+            return false;
+        }
+        // 校验 BCD 字节:每个半字节必须在 0~9 之间
+        for (int i = 2; i <= 4; i++) {
+            if (!isValidBcdByte(frame[i])) {
+                return false;
+            }
+        }
+        // 单位字节只能为 0 或 1
+        if (frame[5] != 0 && frame[5] != 1) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 判断一个字节是否为合法的 BCD 字节(高低 4 位均在 0~9 之间)
+     */
+    private static boolean isValidBcdByte(byte b) {
+        int high = (b >> 4) & 0x0F;
+        int low = b & 0x0F;
+        return high <= 9 && low <= 9;
+    }
+
+    /**
+     * 解析 6 字节数据帧为 WeightData 对象
+     *
+     * @param frame 6 字节数据帧
+     * @return 解析结果;如果帧不合法返回 null
+     */
+    public static WeightData parse(byte[] frame) {
+        if (!validateFrame(frame)) {
+            return null;
+        }
+        return parseInternal(frame);
+    }
+
+    /**
+     * 解析 6 字节数据帧为 WeightData 对象(不做严格校验,仅做位解析)
+     */
+    private static WeightData parseInternal(byte[] frame) {
+        WeightData data = new WeightData();
+        data.setRawHex(bytesToHexString(frame));
+
+        byte statusByte = frame[1];
+        // D0~D2 小数点位数
+        int decimalPlaces = statusByte & 0x07;
+        // D3~D4 工作模式
+        int workMode = (statusByte >> 3) & 0x03;
+        // D5 符号位
+        boolean negative = ((statusByte >> 5) & 0x01) == 1;
+        // D6 稳定状态
+        boolean stable = ((statusByte >> 6) & 0x01) == 1;
+        // D7 溢出状态
+        boolean overflow = ((statusByte >> 7) & 0x01) == 1;
+
+        // BCD 解码:BCD3(高) -> BCD2(中) -> BCD1(低)
+        long rawValue = bcdToLong(frame[5 - 1]); // BCD3 = frame[4] 最高
+        rawValue = rawValue * 100 + bcdToLong(frame[4 - 1]); // BCD2 = frame[3] 中间
+        rawValue = rawValue * 100 + bcdToLong(frame[3 - 1]); // BCD1 = frame[2] 最低
+
+        // 单位
+        String unit = frame[5] == 1 ? WeightData.UNIT_LB : WeightData.UNIT_KG;
+
+        // 实际重量值 = 原始显示值 / (10^小数点位数)
+        double value = rawValue;
+        if (decimalPlaces > 0) {
+            value = rawValue / Math.pow(10, decimalPlaces);
+        }
+        if (negative) {
+            value = -value;
+        }
+
+        data.setDecimalPlaces(decimalPlaces);
+        data.setWorkMode(workMode);
+        data.setNegative(negative);
+        data.setStable(stable);
+        data.setOverflow(overflow);
+        data.setRawDisplayValue(rawValue);
+        data.setUnit(unit);
+        data.setValue(value);
+        return data;
+    }
+
+    /**
+     * 将一个 BCD 字节转换为 0~99 的整数
+     * 高 4 位为十位,低 4 位为个位
+     */
+    public static int bcdToLong(byte bcd) {
+        int high = (bcd >> 4) & 0x0F;
+        int low = bcd & 0x0F;
+        return high * 10 + low;
+    }
+
+    /**
+     * 在缓冲区 [offset, offset+length) 范围内查找第一个帧头 0xFF 的位置。
+     * 不要求剩余字节足够组成完整一帧,调用方需自行判断剩余长度。
+     *
+     * @param buffer 字节缓冲区
+     * @param offset 起始偏移
+     * @param length 有效长度
+     * @return 帧头所在偏移;未找到返回 -1
+     */
+    public static int findFrameStart(byte[] buffer, int offset, int length) {
+        int end = offset + length;
+        for (int i = offset; i < end; i++) {
+            if (isFrameHeader(buffer[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}

+ 352 - 0
src/com/mes/util/WeightSerialService.java

@@ -0,0 +1,352 @@
+package com.mes.util;
+
+import com.fazecast.jSerialComm.SerialPort;
+import com.fazecast.jSerialComm.SerialPortDataListener;
+import com.fazecast.jSerialComm.SerialPortEvent;
+
+import java.io.ByteArrayOutputStream;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 基于 jSerialComm 的称重仪表串口通信服务
+ *
+ * 功能:
+ *  - 串口打开/关闭
+ *  - 串口参数配置(波特率默认 9600、数据位 8、停止位 1、校验位 NONE)
+ *  - 异步数据监听(事件驱动)
+ *  - 数据接收缓存(自动拼接跨事件的不完整帧)
+ *  - 数据帧完整性判断(帧头 0xFF + 长度 + BCD 校验)
+ *  - 通信异常处理(监听器异常捕获,不中断后续接收)
+ *
+ * 使用方式:
+ *   WeightSerialService service = new WeightSerialService();
+ *   service.addListener(data -> System.out.println(data));
+ *   service.open("COM3", 9600);
+ *   ...
+ *   service.close();
+ *
+ * 该类线程安全:可在多个线程调用 open/close,监听回调在 jSerialComm 后台线程触发。
+ */
+public class WeightSerialService {
+
+    /** 默认波特率 */
+    public static final int DEFAULT_BAUD_RATE = 9600;
+    /** 默认数据位 */
+    public static final int DEFAULT_DATA_BITS = 8;
+    /** 默认停止位 */
+    public static final int DEFAULT_STOP_BITS = SerialPort.ONE_STOP_BIT;
+    /** 默认校验位 */
+    public static final int DEFAULT_PARITY = SerialPort.NO_PARITY;
+
+    /** jSerialComm 串口对象 */
+    private SerialPort serialPort;
+
+    /** 当前连接状态 */
+    private final AtomicBoolean connected = new AtomicBoolean(false);
+
+    /** 接收缓存:用于跨事件拼接不完整的数据帧 */
+    private final ByteArrayOutputStream rxBuffer = new ByteArrayOutputStream(64);
+
+    /** 已注册的重量数据监听器 */
+    private final CopyOnWriteArrayList<WeightDataListener> listeners = new CopyOnWriteArrayList<>();
+
+    /** 当前使用的串口配置 */
+    private ParamConfig currentConfig;
+
+    /**
+     * 重量数据回调接口,供上层(UI / MES)注册接收解析后的重量数据
+     */
+    public interface WeightDataListener {
+        /**
+         * 收到一帧合法的重量数据时回调
+         */
+        void onWeightData(WeightData data);
+
+        /**
+         * 串口连接状态变化时回调
+         *
+         * @param connected true=已连接, false=已断开
+         */
+        default void onConnectionChanged(boolean connected) {
+        }
+
+        /**
+         * 发生异常时回调
+         */
+        default void onError(Throwable t) {
+        }
+    }
+
+    public WeightSerialService() {
+    }
+
+    /**
+     * 注册重量数据监听器
+     */
+    public void addListener(WeightDataListener listener) {
+        if (listener != null) {
+            listeners.add(listener);
+        }
+    }
+
+    /**
+     * 移除监听器
+     */
+    public void removeListener(WeightDataListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * 使用 ParamConfig 打开串口
+     */
+    public boolean open(ParamConfig config) {
+        return open(config.getSerialNumber(),
+                config.getBaudRate(),
+                config.getDataBit(),
+                config.getStopBit(),
+                config.getCheckoutBit());
+    }
+
+    /**
+     * 打开串口(使用默认参数)
+     *
+     * @param portName 串口名称,例如 "COM3"
+     * @param baudRate  波特率,例如 9600
+     */
+    public boolean open(String portName, int baudRate) {
+        return open(portName, baudRate, DEFAULT_DATA_BITS, DEFAULT_STOP_BITS, DEFAULT_PARITY);
+    }
+
+    /**
+     * 打开串口(完整参数)
+     *
+     * @param portName  串口名称
+     * @param baudRate  波特率
+     * @param dataBits  数据位 (5/6/7/8)
+     * @param stopBits  停止位 ({@link SerialPort#ONE_STOP_BIT}/{@link SerialPort#TWO_STOP_BIT} 等)
+     * @param parity    校验位 ({@link SerialPort#NO_PARITY}/{@link SerialPort#EVEN_PARITY} 等)
+     */
+    public synchronized boolean open(String portName, int baudRate, int dataBits, int stopBits, int parity) {
+        if (connected.get()) {
+            // 已连接则先关闭
+            close();
+        }
+        try {
+            serialPort = SerialPort.getCommPort(portName);
+            if (serialPort == null) {
+                notifyError(new IllegalStateException("串口不存在: " + portName));
+                return false;
+            }
+            // 设置串口参数
+            serialPort.setBaudRate(baudRate);
+            serialPort.setNumDataBits(dataBits);
+            serialPort.setNumStopBits(stopBits);
+            serialPort.setParity(parity);
+            // 设置超时:非阻塞模式
+            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 0, 0);
+
+            boolean opened = serialPort.openPort();
+            if (!opened) {
+                notifyError(new IllegalStateException("串口打开失败: " + portName));
+                serialPort = null;
+                return false;
+            }
+
+            // 清空接收缓存
+            synchronized (rxBuffer) {
+                rxBuffer.reset();
+            }
+
+            // 注册数据监听器
+            serialPort.addDataListener(new SerialPortDataListener() {
+                @Override
+                public int getListeningEvents() {
+                    return SerialPort.LISTENING_EVENT_DATA_RECEIVED
+                            | SerialPort.LISTENING_EVENT_PORT_DISCONNECTED;
+                }
+
+                @Override
+                public void serialEvent(SerialPortEvent event) {
+                    handleSerialEvent(event);
+                }
+            });
+
+            currentConfig = new ParamConfig(portName, baudRate, parity, dataBits, stopBits);
+            connected.set(true);
+            notifyConnectionChanged(true);
+            return true;
+        } catch (Exception e) {
+            notifyError(e);
+            if (serialPort != null) {
+                try {
+                    serialPort.closePort();
+                } catch (Exception ignored) {
+                }
+                serialPort = null;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 关闭串口并释放资源
+     */
+    public synchronized void close() {
+        if (serialPort != null) {
+            try {
+                serialPort.removeDataListener();
+                serialPort.closePort();
+            } catch (Exception e) {
+                notifyError(e);
+            } finally {
+                serialPort = null;
+            }
+        }
+        synchronized (rxBuffer) {
+            rxBuffer.reset();
+        }
+        if (connected.compareAndSet(true, false)) {
+            notifyConnectionChanged(false);
+        }
+    }
+
+    /**
+     * 处理串口事件
+     */
+    private void handleSerialEvent(SerialPortEvent event) {
+        try {
+            if (event.getEventType() == SerialPort.LISTENING_EVENT_PORT_DISCONNECTED) {
+                connected.set(false);
+                notifyConnectionChanged(false);
+                return;
+            }
+            if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
+                return;
+            }
+            byte[] newData = event.getReceivedData();
+            if (newData == null || newData.length == 0) {
+                return;
+            }
+            processData(newData);
+        } catch (Throwable t) {
+            // 任何异常都不能让监听线程退出
+            notifyError(t);
+        }
+    }
+
+    /**
+     * 处理接收到的字节数据:缓存、组帧、解析。
+     * 算法:
+     *   1. 将新数据追加到接收缓存
+     *   2. 在缓存中查找帧头 0xFF,丢弃帧头之前的无效字节
+     *   3. 若剩余字节不足一帧,保留并等待下一次数据到达
+     *   4. 若够一帧,提取并解析,然后从帧尾继续查找下一帧
+     */
+    private void processData(byte[] newData) {
+        synchronized (rxBuffer) {
+            rxBuffer.write(newData, 0, newData.length);
+            byte[] all = rxBuffer.toByteArray();
+            int frameLen = WeightProtocolParser.FRAME_LENGTH;
+            int totalLen = all.length;
+
+            int pos = 0;
+            while (true) {
+                // 从 pos 开始查找下一个帧头
+                int start = WeightProtocolParser.findFrameStart(all, pos, totalLen - pos);
+                if (start < 0) {
+                    // 没有帧头,丢弃所有字节
+                    rxBuffer.reset();
+                    return;
+                }
+                // 丢弃帧头之前的无效字节,从 start 开始判断是否够一帧
+                if (start + frameLen > totalLen) {
+                    // 剩余字节不足一帧,保留 start 之后的数据等待下次到达
+                    byte[] remain = new byte[totalLen - start];
+                    System.arraycopy(all, start, remain, 0, remain.length);
+                    rxBuffer.reset();
+                    rxBuffer.write(remain, 0, remain.length);
+                    return;
+                }
+                // 提取一帧并解析
+                byte[] frame = new byte[frameLen];
+                System.arraycopy(all, start, frame, 0, frameLen);
+                WeightData data = WeightProtocolParser.parse(frame);
+                if (data != null) {
+                    notifyWeightData(data);
+                }
+                // 移动 pos 到帧尾,继续查找下一帧
+                pos = start + frameLen;
+                if (pos >= totalLen) {
+                    // 全部消费完毕
+                    rxBuffer.reset();
+                    return;
+                }
+            }
+        }
+    }
+
+    /**
+     * 通知所有监听器收到重量数据
+     */
+    private void notifyWeightData(WeightData data) {
+        for (WeightDataListener l : listeners) {
+            try {
+                l.onWeightData(data);
+            } catch (Throwable t) {
+                notifyError(t);
+            }
+        }
+    }
+
+    /**
+     * 通知所有监听器连接状态变化
+     */
+    private void notifyConnectionChanged(boolean isConnected) {
+        for (WeightDataListener l : listeners) {
+            try {
+                l.onConnectionChanged(isConnected);
+            } catch (Throwable t) {
+                notifyError(t);
+            }
+        }
+    }
+
+    /**
+     * 通知所有监听器发生异常
+     */
+    private void notifyError(Throwable t) {
+        for (WeightDataListener l : listeners) {
+            try {
+                l.onError(t);
+            } catch (Throwable ignored) {
+            }
+        }
+    }
+
+    /**
+     * 当前是否已连接
+     */
+    public boolean isConnected() {
+        return connected.get() && serialPort != null && serialPort.isOpen();
+    }
+
+    /**
+     * 获取当前串口配置(仅当 open 成功后有效)
+     */
+    public ParamConfig getCurrentConfig() {
+        return currentConfig;
+    }
+
+    /**
+     * 获取当前可用的串口名称列表
+     */
+    public static String[] listPortNames() {
+        SerialPort[] ports = SerialPort.getCommPorts();
+        String[] names = new String[ports.length];
+        for (int i = 0; i < ports.length; i++) {
+            names[i] = ports[i].getSystemPortName();
+        }
+        return names;
+    }
+}

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

@@ -1,9 +1,9 @@
 #\u5DE5\u4F4D\u53F7
 #XT OP090 OP100 OP110 OP120 OP160 OP180 OP190 OP250 OP260 OP280 OP360 OP370 OP390 OP400
-mes.gw=OP430
+mes.gw=OP480
 #\u670D\u52A1\u5668IP\u5730\u5740
 #mes.server_ip=127.0.0.1
-mes.server_ip=192.168.11.99
+mes.server_ip=192.168.14.99
 #TCP\u7AEF\u53E3
 mes.tcp_port=3000
 #\u5FC3\u8DF3\u5468\u671F