hou 1 hete
szülő
commit
f62f2905ae

+ 3 - 0
src/META-INF/MANIFEST.MF

@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.mes.ui.MesClient
+

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

@@ -3,6 +3,7 @@ package com.mes.ui;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import com.mes.util.JdbcUtils;
+import com.mes.util.Ut5310TestResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -100,8 +101,17 @@ public class DataUtil {
                     + "&deviceType=UT5310"
                     + "&deviceResult=" + encode(deviceResult)
                     + "&deviceData=" + encode(deviceData);
+            System.out.println("========== MES 上传(UT5310) ==========");
+            System.out.println("URL: " + url);
+            System.out.println("oprno=" + oprno + ", lineSn=" + lineSn + ", sn=" + sn
+                    + ", result=" + ret + ", ucode=" + user);
+            System.out.println("deviceType=UT5310, deviceResult=" + deviceResult);
+            System.out.println("deviceData(原始报文)=" + deviceData);
+            System.out.println("POST body=" + params);
             log.info("params=" + params);
             String result = doPost(url, params);
+            System.out.println("MES 响应: " + result);
+            System.out.println("====================================");
             log.info("result=" + result);
 
             if (result == null || result.trim().isEmpty() || result.equalsIgnoreCase("false")) {
@@ -116,6 +126,39 @@ public class DataUtil {
         }
     }
 
+    /** 上传 UT5310 绝缘耐压分步明细到 mes_product_insulation2 */
+    public static JSONObject uploadInsulation2(String sn, String user, Ut5310TestResult testResult) {
+        try {
+            if (testResult == null || testResult.getSteps().isEmpty()) {
+                log.info("uploadInsulation2 skipped: no step data");
+                return null;
+            }
+            String mes_server_ip = MesClient.mes_server_ip;
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductInsulation2/batchUpload";
+            String stepsJson = testResult.toUploadStepsArray().toJSONString();
+            String params = "__ajax=json&sn=" + encode(sn)
+                    + "&ucode=" + encode(user)
+                    + "&steps=" + encode(stepsJson);
+            System.out.println("========== MES 上传(绝缘耐压明细) ==========");
+            System.out.println("URL: " + url);
+            System.out.println("sn=" + sn + ", ucode=" + user);
+            System.out.println("steps=" + stepsJson);
+            log.info("uploadInsulation2 params=" + params);
+            String result = doPost(url, params);
+            System.out.println("MES 响应: " + result);
+            System.out.println("==========================================");
+            log.info("uploadInsulation2 result=" + result);
+
+            if (result == null || result.trim().isEmpty() || result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            return JSONObject.parseObject(result);
+        } catch (Exception e) {
+            log.info("uploadInsulation2 e=" + e.getMessage());
+            return null;
+        }
+    }
+
     private static String encode(String value) throws UnsupportedEncodingException {
         return URLEncoder.encode(value == null ? "" : value, "UTF-8");
     }

+ 1 - 3
src/com/mes/ui/LoginFarme.java

@@ -198,9 +198,7 @@ public class LoginFarme extends JFrame {
 					if (MesClient.jfxPanel2 == null) {
 						String url = "http://" + MesClient.mes_server_ip + ":8980/js/a/mes/mesProcessCheckRecord/ulist?ucode=" + user_id + "&oprno=" + MesClient.mes_gw + "&lineSn=" + MesClient.mes_line_sn;
 						MesClient.jfxPanel2 = new MesWebView(url);
-						MesClient.jfxPanel2.setSize(990, 550);
-						MesClient.jfxPanel2.setBounds(0, 0, 990, 550);
-						MesClient.indexPanelC.add(MesClient.jfxPanel2);
+						MesClient.indexPanelC.add(MesClient.jfxPanel2, BorderLayout.CENTER);
 					}
 
                     MesClient.getMaterailData();

+ 151 - 12
src/com/mes/ui/MesClient.java

@@ -3,6 +3,7 @@ package com.mes.ui;
 import com.alibaba.fastjson2.JSONObject;
 import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
+import com.fazecast.jSerialComm.SerialPort;
 import com.mes.util.DateLocalUtils;
 import com.mes.util.HttpUtils;
 import com.mes.util.JdbcUtils;
@@ -75,6 +76,8 @@ public class MesClient extends JFrame {
     private JTextField softwareIpField;
     private JTextField softwareOpField;
     private JTextField softwareOpNameField;
+    private static final String APP_CFG_UT5310_PORT = "ut5310_port";
+    private JComboBox<String> softwarePortCombo;
     private JButton softwareEditBtn;
     private JButton softwareSaveBtn;
     // 记录上一次非“软件设置”页签,用于密码校验失败时回退
@@ -164,14 +167,19 @@ public class MesClient extends JFrame {
         String plcIp = pro.getProperty("mes.plc_ip");
         mes_ut5310_enable = Boolean.parseBoolean(pro.getProperty("mes.ut5310_enable", "false"));
         Ut5310ScpiService.configure(
-                pro.getProperty("mes.ut5310_port", "COM1"),
+                pro.getProperty("mes.ut5310_port", "COM3"),
                 getIntProperty(pro, "mes.ut5310_baud_rate", 9600),
                 getIntProperty(pro, "mes.ut5310_data_bits", 8),
                 getIntProperty(pro, "mes.ut5310_stop_bits", 1),
                 pro.getProperty("mes.ut5310_parity", "NONE"),
-                getIntProperty(pro, "mes.ut5310_read_timeout_ms", 1000),
-                getIntProperty(pro, "mes.ut5310_test_timeout_ms", 60000),
-                getIntProperty(pro, "mes.ut5310_fetch_interval_ms", 500));
+                getIntProperty(pro, "mes.ut5310_read_timeout_ms", 2000),
+                getIntProperty(pro, "mes.ut5310_test_timeout_ms", 300000),
+                getIntProperty(pro, "mes.ut5310_fetch_interval_ms", 500),
+                pro.getProperty("mes.ut5310_protocol", "MODBUS"),
+                getIntProperty(pro, "mes.ut5310_modbus_slave", 1),
+                getIntProperty(pro, "mes.ut5310_modbus_max_steps", 4),
+                Boolean.parseBoolean(pro.getProperty("mes.ut5310_modbus_send_start", "false")),
+                getIntProperty(pro, "mes.ut5310_modbus_expected_steps", 2));
 
         if (mes_gw == null) {
             mes_gw = "";
@@ -184,6 +192,13 @@ public class MesClient extends JFrame {
             PlcService.setPlcIp(mes_plc_ip);
         }
 
+        // 页面保存的串口优先于 properties 默认值
+        String portFromDb = JdbcUtils.getAppConfig(APP_CFG_UT5310_PORT);
+        if (portFromDb != null && !portFromDb.isEmpty()) {
+            Ut5310ScpiService.setPortName(portFromDb);
+            log.info("UT5310 串口使用本地配置: {}", portFromDb);
+        }
+
         String ipFromDb = JdbcUtils.getServerIpByOp(mes_gw);
         if (ipFromDb != null && !ipFromDb.trim().isEmpty()) {
             mes_server_ip = ipFromDb.trim();
@@ -697,6 +712,22 @@ public class MesClient extends JFrame {
             SwingUtilities.invokeLater(() -> setMenuStatus("当前状态不允许提交绝缘耐压结果", -1));
             return false;
         }
+
+        if (testResult != null && "ERROR".equalsIgnoreCase(testResult.getJudge())) {
+            plcPhase = PLC_PHASE_WAIT_REQUEST;
+            SwingUtilities.invokeLater(() -> {
+                setMenuStatus("UT5310通信失败: " + testResult.getRawResponse(), -1);
+                finish_ok_bt.setEnabled(true);
+                finish_ng_bt.setEnabled(true);
+            });
+            return false;
+        }
+        if (testResult != null && "STOP".equalsIgnoreCase(testResult.getJudge())) {
+            plcPhase = PLC_PHASE_WAIT_REQUEST;
+            SwingUtilities.invokeLater(() -> setMenuStatus("UT5310测试被停止", -1));
+            return false;
+        }
+
         if (!PlcService.ensureConnected()) {
             SwingUtilities.invokeLater(() -> {
                 setMenuStatus("PLC未连接,请检查网络", -1);
@@ -714,6 +745,26 @@ public class MesClient extends JFrame {
             return false;
         }
 
+        if (testResult != null) {
+            testResult.printToConsole();
+        }
+        System.out.println("[UT5310] 准备上传 MES: sn=" + sn + ", mesResult=" + mesResult
+                + ", deviceResult=" + (testResult == null ? "" : testResult.getJudge()));
+
+        JSONObject insObj = DataUtil.uploadInsulation2(sn, user20, testResult);
+        if (insObj == null || insObj.get("result") == null
+                || !insObj.get("result").toString().equalsIgnoreCase("true")) {
+            if (isOk) {
+                PlcService.setTestOk(false);
+            } else {
+                PlcService.setTestNg(false);
+            }
+            String msg = insObj != null && insObj.get("message") != null
+                    ? insObj.getString("message") : "绝缘耐压明细上传失败,请重试";
+            SwingUtilities.invokeLater(() -> setMenuStatus(msg, -1));
+            return false;
+        }
+
         JSONObject retObj = DataUtil.sendQuality(sn, mesResult, user20,
                 testResult == null ? "" : testResult.getJudge(),
                 testResult == null ? "" : testResult.getRawResponse());
@@ -1066,6 +1117,7 @@ public class MesClient extends JFrame {
         toolBar.add(plc_status_menu);
 
         tabbedPane = new JTabbedPane(JTabbedPane.TOP);
+        tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
         tabbedPane.setMinimumSize(new Dimension(400, 50));
         tabbedPane.setFont(new Font("宋体", Font.BOLD, 22));
         contentPane.add(tabbedPane, BorderLayout.CENTER);
@@ -1179,11 +1231,9 @@ public class MesClient extends JFrame {
 
 //		searchScrollPane = new JScrollPane((Component) null);
 
-        indexPanelC = new JPanel();
-        searchScrollPaneDj = new JScrollPane(indexPanelC);
-        indexPanelC.setLayout(null);
+        indexPanelC = new JPanel(new BorderLayout());
 
-        tabbedPane.addTab("开班点检", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), searchScrollPaneDj, null);
+        tabbedPane.addTab("开班点检", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), indexPanelC, null);
 
 
         // ========== 工作记录 Tab(改造:当前工位Tab + 所有工位Tab) ==========
@@ -1300,9 +1350,22 @@ public class MesClient extends JFrame {
         softwareOpNameField.setEditable(false);
         panel.add(softwareOpNameField);
 
+        JLabel portLabel = new JLabel("耐压仪串口:");
+        portLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18));
+        portLabel.setBounds(40, 250, 200, 30);
+        panel.add(portLabel);
+
+        softwarePortCombo = new JComboBox<String>();
+        softwarePortCombo.setFont(new Font("微软雅黑", Font.PLAIN, 18));
+        softwarePortCombo.setBounds(220, 250, 360, 34);
+        softwarePortCombo.setEditable(true);
+        softwarePortCombo.setEnabled(false);
+        refreshSerialPortCombo();
+        panel.add(softwarePortCombo);
+
         softwareEditBtn = new JButton("编辑");
         softwareEditBtn.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        softwareEditBtn.setBounds(220, 270, 140, 44);
+        softwareEditBtn.setBounds(220, 320, 140, 44);
         softwareEditBtn.addActionListener(new ActionListener() {
             @Override
             public void actionPerformed(ActionEvent e) {
@@ -1310,6 +1373,10 @@ public class MesClient extends JFrame {
                 softwareIpField.setEditable(true);
                 softwareOpField.setEditable(true);
                 softwareOpNameField.setEditable(true);
+                refreshSerialPortCombo();
+                if (softwarePortCombo != null) {
+                    softwarePortCombo.setEnabled(true);
+                }
                 if (softwareSaveBtn != null) {
                     softwareSaveBtn.setEnabled(true);
                 }
@@ -1319,7 +1386,7 @@ public class MesClient extends JFrame {
 
         softwareSaveBtn = new JButton("保存");
         softwareSaveBtn.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        softwareSaveBtn.setBounds(380, 270, 140, 44);
+        softwareSaveBtn.setBounds(380, 320, 140, 44);
         softwareSaveBtn.setEnabled(false);
         softwareSaveBtn.addActionListener(new ActionListener() {
             @Override
@@ -1328,11 +1395,16 @@ public class MesClient extends JFrame {
                 String ip = softwareIpField.getText() == null ? "" : softwareIpField.getText().trim();
                 String op = softwareOpField.getText() == null ? "" : softwareOpField.getText().trim();
                 String opName = softwareOpNameField.getText() == null ? "" : softwareOpNameField.getText().trim();
+                String port = getSelectedSerialPort();
 
                 if (ip.isEmpty() || op.isEmpty()) {
                     JOptionPane.showMessageDialog(mesClientFrame, "服务器IP和工位号不能为空", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
                     return;
                 }
+                if (port.isEmpty()) {
+                    JOptionPane.showMessageDialog(mesClientFrame, "耐压仪串口不能为空", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                    return;
+                }
                 if (opName.isEmpty()) {
                     opName = OprnoUtil.getGwDes(mes_line_sn, op);
                 }
@@ -1343,19 +1415,23 @@ public class MesClient extends JFrame {
 
                 // 写入本地配置并刷新显示
                 applyOpConfig(op, opName, ip);
+                applyUt5310PortConfig(port);
                 loadSoftwareSettingsFromDb();
                 softwareIpField.setEditable(false);
                 softwareOpField.setEditable(false);
                 softwareOpNameField.setEditable(false);
+                if (softwarePortCombo != null) {
+                    softwarePortCombo.setEnabled(false);
+                }
                 softwareSaveBtn.setEnabled(false);
-                JOptionPane.showMessageDialog(mesClientFrame, "保存成功,请重启软件", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                JOptionPane.showMessageDialog(mesClientFrame, "保存成功", "提示窗口", JOptionPane.INFORMATION_MESSAGE);
             }
         });
         panel.add(softwareSaveBtn);
 
         JButton reloadBtn = new JButton("从本地配置读取");
         reloadBtn.setFont(new Font("微软雅黑", Font.PLAIN, 18));
-        reloadBtn.setBounds(540, 270, 200, 44);
+        reloadBtn.setBounds(540, 320, 200, 44);
         reloadBtn.addActionListener(new ActionListener() {
             @Override
 
@@ -1370,6 +1446,60 @@ public class MesClient extends JFrame {
         return panel;
     }
 
+    private void refreshSerialPortCombo() {
+        if (softwarePortCombo == null) {
+            return;
+        }
+        String current = getSelectedSerialPort();
+        if (current.isEmpty()) {
+            current = Ut5310ScpiService.getPortName();
+        }
+        softwarePortCombo.removeAllItems();
+        SerialPort[] ports = SerialPort.getCommPorts();
+        if (ports != null) {
+            for (SerialPort p : ports) {
+                if (p != null && p.getSystemPortName() != null) {
+                    softwarePortCombo.addItem(p.getSystemPortName());
+                }
+            }
+        }
+        if (current != null && !current.isEmpty()) {
+            boolean found = false;
+            for (int i = 0; i < softwarePortCombo.getItemCount(); i++) {
+                if (current.equalsIgnoreCase(softwarePortCombo.getItemAt(i))) {
+                    softwarePortCombo.setSelectedIndex(i);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                softwarePortCombo.addItem(current);
+                softwarePortCombo.setSelectedItem(current);
+            }
+        }
+    }
+
+    private String getSelectedSerialPort() {
+        if (softwarePortCombo == null) {
+            return "";
+        }
+        Object selected = softwarePortCombo.getEditor().getItem();
+        if (selected == null) {
+            selected = softwarePortCombo.getSelectedItem();
+        }
+        return selected == null ? "" : selected.toString().trim();
+    }
+
+    private void applyUt5310PortConfig(String port) {
+        if (port == null || port.trim().isEmpty()) {
+            return;
+        }
+        String next = port.trim();
+        Ut5310ScpiService.setPortName(next);
+        JdbcUtils.setAppConfig(APP_CFG_UT5310_PORT, next);
+        log.info("保存耐压仪串口配置: {}", next);
+    }
+
     // 读取并回填“软件设置”页面数据
     private void loadSoftwareSettingsFromDb() {
         String[] cfg = JdbcUtils.getAnyOpIpConfig();
@@ -1384,6 +1514,12 @@ public class MesClient extends JFrame {
             softwareIpField.setText(mes_server_ip == null ? "" : mes_server_ip);
         }
 
+        String portFromDb = JdbcUtils.getAppConfig(APP_CFG_UT5310_PORT);
+        if (portFromDb != null && !portFromDb.isEmpty()) {
+            Ut5310ScpiService.setPortName(portFromDb);
+        }
+        refreshSerialPortCombo();
+
         if (softwareSaveBtn != null) {
             softwareSaveBtn.setEnabled(false);
         }
@@ -1396,6 +1532,9 @@ public class MesClient extends JFrame {
         if (softwareOpNameField != null) {
             softwareOpNameField.setEditable(false);
         }
+        if (softwarePortCombo != null) {
+            softwarePortCombo.setEnabled(false);
+        }
     }
 
     // 进入“软件设置”页签前的密码校验(固定密码:mes)

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

@@ -86,9 +86,61 @@ public class JdbcUtils {
 				")";
 		statement.executeUpdate(opIpConfig);
 
+		String appConfig = "CREATE TABLE if not exists app_config(\n" +
+				"   key VARCHAR(50) PRIMARY KEY,\n" +
+				"   value VARCHAR(200)\n" +
+				")";
+		statement.executeUpdate(appConfig);
+
         statement.close();
     }
 
+	public static String getAppConfig(String key) {
+		try {
+			if (key == null || key.trim().isEmpty()) {
+				return null;
+			}
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			String sql = "SELECT value FROM app_config WHERE key = ?";
+			try (PreparedStatement ps = conn.prepareStatement(sql)) {
+				ps.setString(1, key.trim());
+				try (ResultSet rs = ps.executeQuery()) {
+					if (rs.next()) {
+						String value = rs.getString("value");
+						return value == null ? null : value.trim();
+					}
+				}
+			}
+		} catch (Exception e) {
+			log.info("查询app_config失败: {}", e.getMessage());
+		}
+		return null;
+	}
+
+	public static boolean setAppConfig(String key, String value) {
+		try {
+			if (key == null || key.trim().isEmpty()) {
+				return false;
+			}
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			String sql = "INSERT INTO app_config(key, value) VALUES(?, ?) " +
+					"ON CONFLICT(key) DO UPDATE SET value=excluded.value";
+			try (PreparedStatement ps = conn.prepareStatement(sql)) {
+				ps.setString(1, key.trim());
+				ps.setString(2, value == null ? "" : value.trim());
+				ps.executeUpdate();
+			}
+			return true;
+		} catch (Exception e) {
+			log.info("保存app_config失败: {}", e.getMessage());
+			return false;
+		}
+	}
+
 	public static String getServerIpByOp(String op) {
 		try {
 			if (op == null || op.trim().isEmpty()) {

+ 217 - 0
src/com/mes/util/ModbusRtuClient.java

@@ -0,0 +1,217 @@
+package com.mes.util;
+
+import com.fazecast.jSerialComm.SerialPort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Modbus RTU over RS232/RS485 serial port.
+ */
+public class ModbusRtuClient {
+
+    public static final Logger log = LoggerFactory.getLogger(ModbusRtuClient.class);
+
+    private static final int INTER_FRAME_MS = 5;
+
+    private final SerialPort serialPort;
+    private final int slaveId;
+    private final int readTimeoutMs;
+
+    public ModbusRtuClient(SerialPort serialPort, int slaveId, int readTimeoutMs) {
+        this.serialPort = serialPort;
+        this.slaveId = slaveId & 0xFF;
+        this.readTimeoutMs = readTimeoutMs;
+    }
+
+    public int[] readHoldingRegisters(int startAddress, int quantity) throws IOException {
+        if (quantity <= 0 || quantity > 125) {
+            throw new IOException("Invalid register quantity: " + quantity);
+        }
+        byte[] request = new byte[8];
+        request[0] = (byte) slaveId;
+        request[1] = 0x03;
+        request[2] = (byte) ((startAddress >> 8) & 0xFF);
+        request[3] = (byte) (startAddress & 0xFF);
+        request[4] = (byte) ((quantity >> 8) & 0xFF);
+        request[5] = (byte) (quantity & 0xFF);
+        appendCrc(request, 6);
+
+        byte[] response = transact(request);
+        validateResponse(response, 0x03);
+        int byteCount = response[2] & 0xFF;
+        if (byteCount != quantity * 2) {
+            throw new IOException("Unexpected byte count: " + byteCount);
+        }
+        int[] registers = new int[quantity];
+        for (int i = 0; i < quantity; i++) {
+            int hi = response[3 + i * 2] & 0xFF;
+            int lo = response[4 + i * 2] & 0xFF;
+            registers[i] = (hi << 8) | lo;
+        }
+        return registers;
+    }
+
+    public void writeSingleRegister(int address, int value) throws IOException {
+        writeMultipleRegisters(address, new int[]{value & 0xFFFF});
+    }
+
+    public void writeMultipleRegisters(int startAddress, int[] values) throws IOException {
+        if (values == null || values.length == 0 || values.length > 123) {
+            throw new IOException("Invalid write values");
+        }
+        int byteCount = values.length * 2;
+        byte[] request = new byte[9 + byteCount];
+        request[0] = (byte) slaveId;
+        request[1] = 0x10;
+        request[2] = (byte) ((startAddress >> 8) & 0xFF);
+        request[3] = (byte) (startAddress & 0xFF);
+        request[4] = (byte) ((values.length >> 8) & 0xFF);
+        request[5] = (byte) (values.length & 0xFF);
+        request[6] = (byte) byteCount;
+        for (int i = 0; i < values.length; i++) {
+            request[7 + i * 2] = (byte) ((values[i] >> 8) & 0xFF);
+            request[8 + i * 2] = (byte) (values[i] & 0xFF);
+        }
+        appendCrc(request, 7 + byteCount);
+
+        byte[] response = transact(request);
+        validateResponse(response, 0x10);
+    }
+
+    private byte[] transact(byte[] request) throws IOException {
+        drainInput();
+        int written = serialPort.writeBytes(request, request.length);
+        if (written != request.length) {
+            throw new IOException("Modbus write incomplete");
+        }
+        log.info("Modbus TX: {}", toHex(request));
+        sleepQuiet(INTER_FRAME_MS);
+
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buffer = new byte[256];
+        long deadline = System.currentTimeMillis() + readTimeoutMs;
+        long lastDataTime = 0;
+        while (System.currentTimeMillis() < deadline) {
+            int read = serialPort.readBytes(buffer, buffer.length);
+            if (read > 0) {
+                out.write(buffer, 0, read);
+                lastDataTime = System.currentTimeMillis();
+                byte[] data = out.toByteArray();
+                if (isResponseComplete(data)) {
+                    log.info("Modbus RX: {}", toHex(data));
+                    return data;
+                }
+            } else if (out.size() > 0 && lastDataTime > 0
+                    && System.currentTimeMillis() - lastDataTime > 80) {
+                break;
+            } else {
+                sleepQuiet(20);
+            }
+        }
+        if (out.size() == 0) {
+            throw new IOException("Modbus response timeout");
+        }
+        byte[] partial = out.toByteArray();
+        log.info("Modbus RX(partial): {}", toHex(partial));
+        return partial;
+    }
+
+    private static boolean isResponseComplete(byte[] data) {
+        if (data.length < 5) {
+            return false;
+        }
+        int functionCode = data[1] & 0xFF;
+        if ((functionCode & 0x80) != 0) {
+            return data.length >= 5;
+        }
+        if (functionCode == 0x03) {
+            int byteCount = data[2] & 0xFF;
+            return data.length >= 3 + byteCount + 2;
+        }
+        if (functionCode == 0x10) {
+            return data.length >= 8;
+        }
+        return data.length >= 5;
+    }
+
+    private static void validateResponse(byte[] response, int functionCode) throws IOException {
+        if (response.length < 5) {
+            throw new IOException("Modbus response too short");
+        }
+        if ((response[1] & 0xFF) == (functionCode | 0x80)) {
+            throw new IOException("Modbus exception code: " + (response[2] & 0xFF));
+        }
+        if ((response[1] & 0xFF) != functionCode) {
+            throw new IOException("Unexpected function code: " + (response[1] & 0xFF));
+        }
+        if (!verifyCrc(response)) {
+            throw new IOException("Modbus CRC error");
+        }
+    }
+
+    private void drainInput() {
+        byte[] buf = new byte[256];
+        try {
+            while (serialPort.bytesAvailable() > 0) {
+                serialPort.readBytes(buf, Math.min(buf.length, serialPort.bytesAvailable()));
+            }
+        } catch (Exception ignored) {
+        }
+    }
+
+    static int crc16(byte[] data, int length) {
+        int crc = 0xFFFF;
+        for (int i = 0; i < length; i++) {
+            crc ^= (data[i] & 0xFF);
+            for (int j = 0; j < 8; j++) {
+                if ((crc & 0x0001) != 0) {
+                    crc = (crc >> 1) ^ 0xA001;
+                } else {
+                    crc >>= 1;
+                }
+            }
+        }
+        return crc & 0xFFFF;
+    }
+
+    private static void appendCrc(byte[] frame, int length) {
+        int crc = crc16(frame, length);
+        frame[length] = (byte) (crc & 0xFF);
+        frame[length + 1] = (byte) ((crc >> 8) & 0xFF);
+    }
+
+    private static boolean verifyCrc(byte[] frame) {
+        if (frame.length < 3) {
+            return false;
+        }
+        int crc = crc16(frame, frame.length - 2);
+        int lo = frame[frame.length - 2] & 0xFF;
+        int hi = frame[frame.length - 1] & 0xFF;
+        return crc == ((hi << 8) | lo);
+    }
+
+    static float registersToFloat(int regHi, int regLo) {
+        int bits = ((regHi & 0xFFFF) << 16) | (regLo & 0xFFFF);
+        return Float.intBitsToFloat(bits);
+    }
+
+    static String toHex(byte[] data) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : data) {
+            sb.append(String.format("%02X ", b & 0xFF));
+        }
+        return sb.toString().trim();
+    }
+
+    private static void sleepQuiet(int ms) throws IOException {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IOException("Modbus interrupted", e);
+        }
+    }
+}

+ 5 - 1
src/com/mes/util/PlcService.java

@@ -94,6 +94,10 @@ public class PlcService {
         return writeMesBit(Op310PlcPoint.TEST_OK_BIT, value);
     }
 
+    public static void main(String[] args) {
+        setTestOk(false);
+    }
+
     public static boolean setTestNg(boolean value) {
         return writeMesBit(Op310PlcPoint.TEST_NG_BIT, value);
     }
@@ -147,7 +151,7 @@ public class PlcService {
                 current[0] = (byte) (current[0] & ~(1 << bitOffset));
             }
             connector.write(DaveArea.DB, Op310PlcPoint.DB_NUMBER, byteOffset, current);
-            log.info("PLC写入 DB{}.DBB{}.{} = {}", Op310PlcPoint.DB_NUMBER, byteOffset, bitOffset, value);
+//            log.info("PLC写入 DB{}.DBB{}.{} = {}", Op310PlcPoint.DB_NUMBER, byteOffset, bitOffset, value);
             return true;
         } catch (Exception e) {
             connected = false;

+ 375 - 78
src/com/mes/util/Ut5310ScpiService.java

@@ -5,35 +5,75 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
 
+/**
+ * UT5310 绝缘耐压仪通信(Modbus RTU,参考 UNI-T Programming Manual MODBUS RTU)。
+ */
 public class Ut5310ScpiService {
 
     public static final Logger log = LoggerFactory.getLogger(Ut5310ScpiService.class);
 
-    private static final Charset COMMAND_CHARSET = StandardCharsets.US_ASCII;
-    private static final String START_COMMAND = "FUNC:START";
-    private static final String FETCH_COMMAND = "FETCh?";
+    public enum Protocol {
+        MODBUS, SCPI, AT, AUTO
+    }
+
+    /** 测试结果起始寄存器 0x0100:步骤1电压,每步占5个寄存器 */
+    private static final int REG_RESULT_START = 0x0100;
+    /**
+     * 每步 Modbus 寄存器布局(手册 3.1):
+     * +0,+1 测试电压(32位浮点,单位 kV)
+     * +2,+3 测试电流或电阻(32位浮点;仪器原始单位:绝缘步骤=MΩ,耐压步骤=mA;显示时转为GΩ/μA)
+     * +4    判读结果(16位整数,0=测试中;0x0003=PASS 等)
+     */
+    private static final int REGS_PER_STEP = 5;
+    /** 启停测试寄存器 0x0500:写 0x0002 启动,0x0000 停止 */
+    private static final int REG_START_STOP = 0x0500;
+    private static final int CMD_START = 0x0002;
+    private static final int CMD_STOP = 0x0000;
+    /** 判读码:PASS(手册示例 0x0003) */
+    private static final int SORT_PASS = 0x0003;
 
     private static String portName = "COM1";
     private static int baudRate = 9600;
     private static int dataBits = 8;
     private static int stopBits = SerialPort.ONE_STOP_BIT;
     private static int parity = SerialPort.NO_PARITY;
-    private static int readTimeoutMs = 1000;
-    private static int testTimeoutMs = 60000;
+    private static int readTimeoutMs = 2000;
+    private static int testTimeoutMs = 300000;
     private static int fetchIntervalMs = 500;
+    private static Protocol protocol = Protocol.MODBUS;
+    private static int modbusSlaveId = 1;
+    /** 单次 Modbus 读取的最大步骤数(寄存器块长度) */
+    private static int modbusMaxSteps = 4;
+    /** 测试文件实际步骤数,全部步骤判读非0后才提交结果(如 PANEL_01=2:绝缘电阻+直流耐压) */
+    private static int modbusExpectedSteps = 2;
+    /** false=仅读取PLC/工装已触发的测试结果,不在Modbus上重复发START */
+    private static boolean modbusSendStart = false;
 
     private static SerialPort serialPort;
-    private static InputStream inputStream;
-    private static OutputStream outputStream;
     private static volatile boolean connected = false;
+    private static volatile Protocol lastUsedProtocol;
 
     public static synchronized void configure(String port, int baud, int data, int stop, String parityName,
                                               int readTimeout, int testTimeout, int fetchInterval) {
+        configure(port, baud, data, stop, parityName, readTimeout, testTimeout, fetchInterval,
+                "MODBUS", 1, 4, false, 2);
+    }
+
+    public static synchronized void configure(String port, int baud, int data, int stop, String parityName,
+                                              int readTimeout, int testTimeout, int fetchInterval,
+                                              String protocolName, int slaveId, int maxSteps,
+                                              boolean sendStart) {
+        configure(port, baud, data, stop, parityName, readTimeout, testTimeout, fetchInterval,
+                protocolName, slaveId, maxSteps, sendStart, 2);
+    }
+
+    public static synchronized void configure(String port, int baud, int data, int stop, String parityName,
+                                              int readTimeout, int testTimeout, int fetchInterval,
+                                              String protocolName, int slaveId, int maxSteps,
+                                              boolean sendStart, int expectedSteps) {
         if (port != null && !port.trim().isEmpty()) {
             portName = port.trim();
         }
@@ -54,6 +94,40 @@ public class Ut5310ScpiService {
         if (fetchInterval > 0) {
             fetchIntervalMs = fetchInterval;
         }
+        protocol = parseProtocol(protocolName);
+        if (slaveId >= 0 && slaveId <= 255) {
+            modbusSlaveId = slaveId;
+        }
+        if (maxSteps > 0 && maxSteps <= 20) {
+            modbusMaxSteps = maxSteps;
+        }
+        modbusSendStart = sendStart;
+        if (expectedSteps > 0 && expectedSteps <= 20) {
+            modbusExpectedSteps = expectedSteps;
+        }
+    }
+
+    public static Protocol getLastUsedProtocol() {
+        return lastUsedProtocol;
+    }
+
+    public static synchronized String getPortName() {
+        return portName;
+    }
+
+    /** 仅更新串口号;若变更则断开,下次通信时按新口重连 */
+    public static synchronized void setPortName(String port) {
+        if (port == null || port.trim().isEmpty()) {
+            return;
+        }
+        String next = port.trim();
+        if (!next.equalsIgnoreCase(portName)) {
+            portName = next;
+            disconnect();
+            log.info("UT5310 port set to {}", portName);
+        } else {
+            portName = next;
+        }
     }
 
     public static synchronized boolean isConnected() {
@@ -65,16 +139,15 @@ public class Ut5310ScpiService {
         try {
             serialPort = SerialPort.getCommPort(portName);
             serialPort.setComPortParameters(baudRate, dataBits, stopBits, parity);
-            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, readTimeoutMs, readTimeoutMs);
+            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_NONBLOCKING, 0, 0);
             if (!serialPort.openPort()) {
                 connected = false;
                 log.info("UT5310 open port failed: {}", portName);
                 return false;
             }
-            inputStream = serialPort.getInputStream();
-            outputStream = serialPort.getOutputStream();
             connected = true;
-            log.info("UT5310 connected: {} {}bps", portName, baudRate);
+            log.info("UT5310 connected: {} {}bps, protocol={}, slave={}",
+                    portName, baudRate, protocol, modbusSlaveId);
             return true;
         } catch (Exception e) {
             connected = false;
@@ -86,10 +159,6 @@ public class Ut5310ScpiService {
 
     public static synchronized void disconnect() {
         connected = false;
-        closeQuietly(inputStream);
-        closeQuietly(outputStream);
-        inputStream = null;
-        outputStream = null;
         if (serialPort != null) {
             try {
                 serialPort.closePort();
@@ -111,76 +180,318 @@ public class Ut5310ScpiService {
             throw new IOException("UT5310 not connected");
         }
 
-        drainInput();
-        writeCommand(START_COMMAND);
-        log.info("UT5310 command sent: {}", START_COMMAND);
+        lastUsedProtocol = protocol == Protocol.AUTO ? Protocol.MODBUS : protocol;
+        System.out.println("[UT5310] 使用协议: Modbus RTU, 站号=" + modbusSlaveId
+                + ", 等待步骤数=" + modbusExpectedSteps);
+        log.info("UT5310 using Modbus RTU, slave={}, expectedSteps={}", modbusSlaveId, modbusExpectedSteps);
+
+        Ut5310TestResult result = runModbusTest();
+        if (result != null) {
+            result.printToConsole();
+        }
+        return result;
+    }
+
+    private static Ut5310TestResult runModbusTest() throws IOException {
+        ModbusRtuClient client = new ModbusRtuClient(serialPort, modbusSlaveId, readTimeoutMs);
+        int registerCount = modbusMaxSteps * REGS_PER_STEP;
+
+        // 先尝试直接读取(工装/PLC 可能已完成测试)
+        Ut5310TestResult existing = tryReadModbusResult(client, registerCount);
+        if (existing != null) {
+            System.out.println("[UT5310] 读取到已有测试结果(未发START)");
+            return existing;
+        }
+
+        if (modbusSendStart) {
+            System.out.println("[UT5310] Modbus 写 0x0500: STOP -> START");
+            log.info("Modbus write 0x0500 STOP then START");
+            client.writeSingleRegister(REG_START_STOP, CMD_STOP);
+            sleepQuiet(200);
+            client.writeSingleRegister(REG_START_STOP, CMD_START);
+            sleepQuiet(500);
+            int runState = readStartStopState(client);
+            System.out.println("[UT5310] 0x0500 当前值=0x" + Integer.toHexString(runState).toUpperCase());
+            log.info("Modbus 0x0500 state=0x{}", Integer.toHexString(runState));
+
+            existing = tryReadModbusResult(client, registerCount);
+            if (existing != null) {
+                return existing;
+            }
+        } else {
+            System.out.println("[UT5310] 仅读取Modbus结果,不发送START( mes.ut5310_modbus_send_start=false )");
+            log.info("Modbus read-only mode, skip START command");
+        }
 
         long deadline = System.currentTimeMillis() + testTimeoutMs;
-        String lastResponse = "";
+        int pollCount = 0;
         while (System.currentTimeMillis() < deadline) {
-            sleep(fetchIntervalMs);
-            writeCommand(FETCH_COMMAND);
-            lastResponse = readResponse();
-            log.info("UT5310 fetch response: {}", lastResponse);
-
-            Ut5310TestResult result = parseFetchResult(lastResponse);
+            pollCount++;
+            int[] regs = client.readHoldingRegisters(REG_RESULT_START, registerCount);
+            if (pollCount == 1 || pollCount % 10 == 0) {
+                logPollProgress(pollCount, regs);
+            }
+            Ut5310TestResult result = parseModbusResult(regs);
             if (result != null) {
                 return result;
             }
+            sleepQuiet(fetchIntervalMs);
         }
-        throw new IOException("UT5310 test timeout, last response: " + lastResponse);
+
+        if (modbusSendStart) {
+            try {
+                client.writeSingleRegister(REG_START_STOP, CMD_STOP);
+            } catch (Exception e) {
+                log.info("Modbus stop ignored: {}", e.getMessage());
+            }
+        }
+
+        throw new IOException("UT5310 Modbus 超时:"
+                + formatCompletionStatus(null)
+                + "。请增大 mes.ut5310_test_timeout_ms(当前" + testTimeoutMs + "ms)");
     }
 
-    public static Ut5310TestResult parseFetchResult(String response) {
-        if (response == null) {
-            return null;
+    /** 轮询时打印各步骤实时电压、电阻/电流、判读(判读仅该步骤结束后才有值) */
+    private static void logPollProgress(int pollCount, int[] regs) {
+        int steps = Math.min(modbusExpectedSteps, regs.length / REGS_PER_STEP);
+        StringBuilder sb = new StringBuilder();
+        sb.append(String.format("[UT5310] 轮询#%d:", pollCount));
+        for (int i = 0; i < steps; i++) {
+            int base = i * REGS_PER_STEP;
+            float volt = ModbusRtuClient.registersToFloat(regs[base], regs[base + 1]);
+            float rawData = ModbusRtuClient.registersToFloat(regs[base + 2], regs[base + 3]);
+            float displayData = convertStepData(i, rawData);
+            int sort = regs[base + 4];
+            sb.append(String.format(" [%d]%s 电压=%.3fkV %s=%s 判读=%s",
+                    i + 1, stepTypeName(i), volt, stepDataUnit(i),
+                    formatStepData(i, displayData), sortingToChinese(sort)));
         }
-        String raw = response.trim();
-        if (raw.isEmpty()) {
+        System.out.println(sb);
+        int[] sorting = extractSorting(regs);
+        if (modbusExpectedSteps > 1 && sorting[0] != 0 && sorting.length > 1 && sorting[1] == 0) {
+            System.out.println("[UT5310] 步骤1(" + stepTypeName(0) + ")已判读完成,等待步骤2("
+                    + stepTypeName(1) + ")结束后再提交...");
+        }
+    }
+
+    private static String formatCompletionStatus(int[] sorting) {
+        if (sorting == null) {
+            return "未完成全部" + modbusExpectedSteps + "个测试步骤";
+        }
+        StringBuilder sb = new StringBuilder("已完成");
+        int done = 0;
+        for (int i = 0; i < modbusExpectedSteps && i < sorting.length; i++) {
+            if (sorting[i] != 0) {
+                done++;
+            }
+        }
+        sb.append(done).append('/').append(modbusExpectedSteps).append("步");
+        return sb.toString();
+    }
+
+    private static Ut5310TestResult tryReadModbusResult(ModbusRtuClient client, int registerCount) throws IOException {
+        int[] regs = client.readHoldingRegisters(REG_RESULT_START, registerCount);
+        return parseModbusResult(regs);
+    }
+
+    private static Ut5310TestResult parseModbusResult(int[] regs) {
+        int[] sorting = extractSorting(regs);
+        float[] voltages = extractVoltage(regs);
+        float[] values = extractValue(regs);
+        if (!isSortingComplete(sorting, voltages, values)) {
             return null;
         }
-        String normalized = raw.toUpperCase();
-        if (normalized.contains("FAIL") || normalized.contains("NG")) {
-            return new Ut5310TestResult(false, "FAIL", raw);
+        return buildModbusResult(sorting, voltages, values);
+    }
+
+    private static int readStartStopState(ModbusRtuClient client) {
+        try {
+            int[] state = client.readHoldingRegisters(REG_START_STOP, 1);
+            return state[0];
+        } catch (Exception e) {
+            log.info("Read 0x0500 failed: {}", e.getMessage());
+            return -1;
         }
-        if (normalized.contains("PASS") || normalized.equals("OK") || normalized.startsWith("OK,")) {
-            return new Ut5310TestResult(true, "PASS", raw);
+    }
+
+    /** 从寄存器块提取各步骤判读值(每步偏移+4) */
+    private static int[] extractSorting(int[] regs) {
+        int steps = regs.length / REGS_PER_STEP;
+        int[] sorting = new int[steps];
+        for (int i = 0; i < steps; i++) {
+            sorting[i] = regs[i * REGS_PER_STEP + 4];
         }
-        return null;
+        return sorting;
     }
 
-    private static void writeCommand(String command) throws IOException {
-        outputStream.write((command + "\n").getBytes(COMMAND_CHARSET));
-        outputStream.flush();
+    /** 从寄存器块提取各步骤测试电压(kV,每步偏移+0/+1) */
+    private static float[] extractVoltage(int[] regs) {
+        int steps = regs.length / REGS_PER_STEP;
+        float[] values = new float[steps];
+        for (int i = 0; i < steps; i++) {
+            int base = i * REGS_PER_STEP;
+            values[i] = ModbusRtuClient.registersToFloat(regs[base], regs[base + 1]);
+        }
+        return values;
     }
 
-    private static String readResponse() throws IOException {
-        StringBuilder response = new StringBuilder();
-        byte[] buffer = new byte[256];
-        long deadline = System.currentTimeMillis() + readTimeoutMs;
-        while (System.currentTimeMillis() < deadline) {
-            int len = inputStream.read(buffer);
-            if (len > 0) {
-                response.append(new String(buffer, 0, len, COMMAND_CHARSET));
-                if (response.indexOf("\n") >= 0) {
-                    break;
-                }
-            } else if (response.length() > 0) {
-                break;
+    /** 从寄存器块提取各步骤电流或电阻(仪器原始:绝缘=MΩ,耐压=mA) */
+    private static float[] extractValue(int[] regs) {
+        int steps = regs.length / REGS_PER_STEP;
+        float[] values = new float[steps];
+        for (int i = 0; i < steps; i++) {
+            int base = i * REGS_PER_STEP;
+            values[i] = ModbusRtuClient.registersToFloat(regs[base + 2], regs[base + 3]);
+        }
+        return values;
+    }
+
+    /**
+     * 全部配置步骤的判读寄存器均非0时,才视为整次测试结束并提交MES/PLC。
+     * 避免步骤1(绝缘电阻)完成后过早提交、工装抬起而步骤2(直流耐压)仍在进行。
+     */
+    static boolean isSortingComplete(int[] sorting, float[] voltages, float[] values) {
+        int expected = Math.min(modbusExpectedSteps, sorting.length);
+        if (expected <= 0) {
+            expected = 1;
+        }
+        for (int i = 0; i < expected; i++) {
+            if (sorting[i] == 0) {
+                return false;
             }
         }
-        return response.toString().trim();
+        return true;
     }
 
-    private static void drainInput() {
-        try {
-            while (inputStream != null && inputStream.available() > 0) {
-                inputStream.read();
+    /** PANEL_01 等测试文件:步骤1绝缘电阻,步骤2直流耐压 */
+    private static String stepTypeName(int stepIndex) {
+        switch (stepIndex) {
+            case 0:
+                return "绝缘电阻";
+            case 1:
+                return "直流耐压";
+            default:
+                return "测试";
+        }
+    }
+
+    private static String stepDataUnit(int stepIndex) {
+        switch (stepIndex) {
+            case 0:
+                return "电阻(GΩ)";
+            case 1:
+                return "电流(μA)";
+            default:
+                return "测试值";
+        }
+    }
+
+    /** 仪器原始值转显示单位:MΩ→GΩ,mA→μA */
+    private static float convertStepData(int stepIndex, float rawValue) {
+        if (stepIndex == 0) {
+            return rawValue / 1000f;
+        }
+        if (stepIndex == 1) {
+            return rawValue * 1000f;
+        }
+        return rawValue;
+    }
+
+    private static String formatStepData(int stepIndex, float displayValue) {
+        if (stepIndex == 0) {
+            return String.format("%.4f", displayValue);
+        }
+        if (stepIndex == 1) {
+            return String.format("%.2f", displayValue);
+        }
+        return String.format("%.6f", displayValue);
+    }
+
+    private static String sortingToChinese(int sortCode) {
+        if (sortCode == 0) {
+            return "0x0000(测试中)";
+        }
+        return sortingToJudge(sortCode) + "(0x" + Integer.toHexString(sortCode).toUpperCase() + ")";
+    }
+
+    private static Ut5310TestResult buildModbusResult(int[] sorting, float[] voltages, float[] values) {
+        List<Ut5310TestResult.StepData> steps = new ArrayList<>();
+        StringBuilder raw = new StringBuilder();
+        raw.append("ModbusRTU,slave=").append(modbusSlaveId).append('\n');
+
+        boolean allPass = true;
+        String totalJudge = "OK";
+        int activeSteps = 0;
+
+        for (int i = 0; i < sorting.length; i++) {
+            if (sorting[i] == 0) {
+                continue;
             }
-        } catch (Exception ignored) {
+            activeSteps++;
+            int stepNo = i + 1;
+            String stepJudge = sortingToJudge(sorting[i]);
+            if (!"OK".equals(stepJudge) && !"PASS".equals(stepJudge)) {
+                allPass = false;
+                totalJudge = stepJudge;
+            }
+            float volt = voltages[i];
+            float displayData = convertStepData(i, values[i]);
+            String typeName = stepTypeName(i);
+            String dataUnit = stepDataUnit(i);
+            String dataText = formatStepData(i, displayData);
+            raw.append(String.format("步骤%d(%s),判读=0x%04X(%s),电压=%.4fkV,%s=%s%n",
+                    stepNo, typeName, sorting[i], stepJudge, volt, dataUnit, dataText));
+            steps.add(new Ut5310TestResult.StepData(
+                    stepNo, typeName, stepJudge,
+                    "", "", String.format("%.4f", volt), "",
+                    dataText));
+            System.out.println(String.format("[UT5310] 步骤%d(%s): 判读=%s, 电压=%.4fkV, %s=%s",
+                    stepNo, typeName, sortingToChinese(sorting[i]), volt, dataUnit, dataText));
+        }
+
+        if (activeSteps == 0) {
+            return new Ut5310TestResult(false, "FAIL", raw.toString());
+        }
+        if (allPass) {
+            totalJudge = "OK";
+        }
+        System.out.println("[UT5310] 全部" + modbusExpectedSteps + "个测试步骤已完成,提交结果");
+        return new Ut5310TestResult(allPass, totalJudge, raw.toString(), totalJudge, steps);
+    }
+
+    static String sortingToJudge(int sortCode) {
+        if (sortCode == SORT_PASS) {
+            return "OK";
+        }
+        switch (sortCode) {
+            case 0x0100: return "短路";
+            case 0x0101: return "电弧";
+            case 0x0110: return "接地失败";
+            case 0x0111: return "过压";
+            case 0x1000: return "上限不良";
+            case 0x1001: return "下限不良";
+            case 0x1010: return "充电下限";
+            default: return "NG(0x" + Integer.toHexString(sortCode).toUpperCase() + ")";
         }
     }
 
+    private static Protocol parseProtocol(String value) {
+        if (value == null) {
+            return Protocol.MODBUS;
+        }
+        String v = value.trim().toUpperCase();
+        if ("SCPI".equals(v)) {
+            return Protocol.SCPI;
+        }
+        if ("AT".equals(v) || "TEST".equals(v)) {
+            return Protocol.AT;
+        }
+        if ("AUTO".equals(v)) {
+            return Protocol.AUTO;
+        }
+        return Protocol.MODBUS;
+    }
+
     private static int toStopBits(int stop) {
         if (stop == 2) {
             return SerialPort.TWO_STOP_BITS;
@@ -202,7 +513,7 @@ public class Ut5310ScpiService {
         return SerialPort.NO_PARITY;
     }
 
-    private static void sleep(int millis) throws IOException {
+    private static void sleepQuiet(int millis) throws IOException {
         try {
             Thread.sleep(millis);
         } catch (InterruptedException e) {
@@ -210,18 +521,4 @@ public class Ut5310ScpiService {
             throw new IOException("UT5310 test interrupted", e);
         }
     }
-
-    private static void closeQuietly(Object closeable) {
-        if (closeable == null) {
-            return;
-        }
-        try {
-            if (closeable instanceof InputStream) {
-                ((InputStream) closeable).close();
-            } else if (closeable instanceof OutputStream) {
-                ((OutputStream) closeable).close();
-            }
-        } catch (Exception ignored) {
-        }
-    }
 }

+ 179 - 0
src/com/mes/util/Ut5310TestResult.java

@@ -1,15 +1,91 @@
 package com.mes.util;
 
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 public class Ut5310TestResult {
 
+    public static class StepData {
+
+        private final int step;
+        private final String func;
+        private final String stepJudge;
+        private final String time;
+        private final String scan;
+        private final String volt;
+        private final String limit;
+        private final String dataValue;
+
+        public StepData(int step, String func, String stepJudge,
+                        String time, String scan, String volt, String limit, String dataValue) {
+            this.step = step;
+            this.func = func;
+            this.stepJudge = stepJudge;
+            this.time = time;
+            this.scan = scan;
+            this.volt = volt;
+            this.limit = limit;
+            this.dataValue = dataValue;
+        }
+
+        public int getStep() {
+            return step;
+        }
+
+        public String getFunc() {
+            return func;
+        }
+
+        public String getStepJudge() {
+            return stepJudge;
+        }
+
+        public String getTime() {
+            return time;
+        }
+
+        public String getScan() {
+            return scan;
+        }
+
+        public String getVolt() {
+            return volt;
+        }
+
+        public String getLimit() {
+            return limit;
+        }
+
+        public String getDataValue() {
+            return dataValue;
+        }
+
+        public boolean isOk() {
+            return "OK".equalsIgnoreCase(stepJudge) || "PASS".equalsIgnoreCase(stepJudge);
+        }
+    }
+
     private final boolean ok;
     private final String judge;
     private final String rawResponse;
+    private final String totalJudge;
+    private final List<StepData> steps;
 
     public Ut5310TestResult(boolean ok, String judge, String rawResponse) {
+        this(ok, judge, rawResponse, null, Collections.emptyList());
+    }
+
+    public Ut5310TestResult(boolean ok, String judge, String rawResponse,
+                            String totalJudge, List<StepData> steps) {
         this.ok = ok;
         this.judge = judge;
         this.rawResponse = rawResponse;
+        this.totalJudge = totalJudge;
+        this.steps = steps == null ? Collections.emptyList() : new ArrayList<>(steps);
     }
 
     public boolean isOk() {
@@ -23,4 +99,107 @@ public class Ut5310TestResult {
     public String getRawResponse() {
         return rawResponse;
     }
+
+    public String getTotalJudge() {
+        return totalJudge;
+    }
+
+    public List<StepData> getSteps() {
+        return Collections.unmodifiableList(steps);
+    }
+
+    public Integer getStep() {
+        if (steps.isEmpty()) {
+            return null;
+        }
+        return steps.get(steps.size() - 1).getStep();
+    }
+
+    public String getFunc() {
+        return lastStepField(StepData::getFunc);
+    }
+
+    public String getStepJudge() {
+        return lastStepField(StepData::getStepJudge);
+    }
+
+    public String getTime() {
+        return lastStepField(StepData::getTime);
+    }
+
+    public String getScan() {
+        return lastStepField(StepData::getScan);
+    }
+
+    public String getVolt() {
+        return lastStepField(StepData::getVolt);
+    }
+
+    public String getLimit() {
+        return lastStepField(StepData::getLimit);
+    }
+
+    public String getDataValue() {
+        return lastStepField(StepData::getDataValue);
+    }
+
+    private String lastStepField(java.util.function.Function<StepData, String> getter) {
+        if (steps.isEmpty()) {
+            return null;
+        }
+        return getter.apply(steps.get(steps.size() - 1));
+    }
+
+    public void printToConsole() {
+        System.out.println("========== UT5310 原始报文(Modbus RTU) ==========");
+        System.out.println(rawResponse == null ? "" : rawResponse);
+        System.out.println("========== UT5310 解析结果 ==========");
+        System.out.println("总判定(ok): " + ok);
+        System.out.println("总判定(judge): " + safe(judge));
+        if (totalJudge != null) {
+            System.out.println("RU总判读: " + totalJudge);
+        }
+        if (steps.isEmpty()) {
+            System.out.println("(无分步明细)");
+        } else {
+            for (StepData step : steps) {
+                System.out.println("--- 步骤 " + step.getStep() + " (" + safe(step.getFunc()) + ") ---");
+                System.out.println("测试类型: " + safe(step.getFunc()));
+                System.out.println("当步判读: " + safe(step.getStepJudge()));
+                System.out.println("测试电压(kV): " + safe(step.getVolt()));
+                System.out.println("测试值(电阻GΩ或电流μA): " + safe(step.getDataValue()));
+            }
+        }
+        System.out.println("====================================");
+    }
+
+    private static String safe(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    /** 转为 mesProductInsulation2/batchUpload 接口所需的 steps JSON */
+    public JSONArray toUploadStepsArray() {
+        JSONArray arr = new JSONArray();
+        for (StepData step : steps) {
+            JSONObject obj = new JSONObject();
+            obj.put("step", String.valueOf(step.getStep()));
+            obj.put("voltage", safe(step.getVolt()));
+            obj.put("voltageUnit", "kV");
+            obj.put("data", safe(step.getDataValue()));
+            obj.put("dataUnit", dataUnitForStep(step.getStep()));
+            obj.put("result", step.isOk() ? "OK" : "NG");
+            arr.add(obj);
+        }
+        return arr;
+    }
+
+    private static String dataUnitForStep(int stepNo) {
+        if (stepNo == 1) {
+            return "GΩ";
+        }
+        if (stepNo == 2) {
+            return "μA";
+        }
+        return "";
+    }
 }

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

@@ -5,11 +5,18 @@
 mes.line_sn=XT
 mes.plc_ip=192.168.0.3
 mes.ut5310_enable=true
-mes.ut5310_port=COM1
+mes.ut5310_port=COM8
 mes.ut5310_baud_rate=9600
 mes.ut5310_data_bits=8
 mes.ut5310_stop_bits=1
 mes.ut5310_parity=NONE
-mes.ut5310_read_timeout_ms=1000
-mes.ut5310_test_timeout_ms=60000
+mes.ut5310_read_timeout_ms=2000
+mes.ut5310_test_timeout_ms=300000
 mes.ut5310_fetch_interval_ms=500
+mes.ut5310_protocol=MODBUS
+mes.ut5310_modbus_slave=1
+mes.ut5310_modbus_max_steps=4
+# 测试文件实际步骤数:PANEL_01=2(步骤1绝缘电阻 + 步骤2直流耐压),全部判读完成后才提交
+mes.ut5310_modbus_expected_steps=2
+# false=工装/PLC已触发测试,MES只读结果;true=MES通过0x0500发START
+mes.ut5310_modbus_send_start=true