liuwei 2 päivää sitten
vanhempi
commit
8256994f8b

+ 3 - 0
.classpath

@@ -20,4 +20,7 @@
 	<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/slf4j-api-1.7.36.jar"/>
+	<classpathentry kind="lib" path="lib/logback-classic-1.2.13.jar"/>
+	<classpathentry kind="lib" path="lib/logback-core-1.2.13.jar"/>
 </classpath>

+ 36 - 0
.cursor/rules/java8-utf8.mdc

@@ -0,0 +1,36 @@
+---
+description: 本项目 Java 必须使用 UTF-8 编码与 JDK 8 编译运行
+alwaysApply: true
+---
+
+# Java 编码与 JDK 版本(常驻)
+
+本仓库是 **Java 8** 工程(class 文件版本 52.0)。
+
+## 强制要求
+
+- 源码与 `javac` 一律使用 **UTF-8**(`-encoding UTF-8`)
+- 编译目标必须是 **JDK 8 / Java 1.8**(`-source 1.8 -target 1.8`,class major=52)
+- **禁止**用本机默认 JDK 11+ 直接 `javac` 产出 class(会导致 IDEA 报:类文件版本 55.0,应为 52.0)
+
+## 推荐本机 JDK 8
+
+优先使用:
+
+```text
+D:\java\jdk-8u202\bin\javac.exe
+```
+
+若无此路径,再用 `D:\java\jdk-8\bin\javac.exe`,或用户已配置的 JDK 8。
+
+## 正确编译示例
+
+```powershell
+$javac8 = "D:\java\jdk-8u202\bin\javac.exe"
+$cp = ((Get-ChildItem lib\*.jar).FullName -join ";") + ";bin"
+& $javac8 -encoding UTF-8 -source 1.8 -target 1.8 -cp $cp -d bin <源文件...>
+```
+
+## 出错时
+
+若出现 `类文件具有错误的版本 55.0, 应为 52.0`:删除对应 `bin/**/*.class` 后,用上面的 JDK 8 命令重编。

+ 45 - 0
.cursor/skills/java8-utf8-compile/SKILL.md

@@ -0,0 +1,45 @@
+---
+name: java8-utf8-compile
+description: >-
+  Compile or rebuild this Java 8 MES client with UTF-8 encoding and JDK 8 only.
+  Use whenever compiling Java, running javac, fixing class file version errors
+  (55.0 vs 52.0), or building mesclient-qmOP180A / similar Java 8 projects.
+---
+
+# Java 8 + UTF-8 编译
+
+## When to use
+
+- 任何 `javac` / Rebuild / 编译失败
+- 报错:`类文件具有错误的版本 55.0, 应为 52.0`
+
+## Rules
+
+1. Always pass `-encoding UTF-8`
+2. Always target Java 8: `-source 1.8 -target 1.8` (class major **52**)
+3. Never leave Java 11+ class files (major **55**) in `bin/`
+
+## Compiler
+
+Prefer:
+
+```text
+D:\java\jdk-8u202\bin\javac.exe
+```
+
+Fallback: `D:\java\jdk-8\bin\javac.exe`
+
+## Example
+
+```powershell
+$javac8 = "D:\java\jdk-8u202\bin\javac.exe"
+$src = "src"
+$out = "bin"
+$lib = "lib"
+$cp = ((Get-ChildItem "$lib\*.jar").FullName -join ";") + ";$out"
+& $javac8 -encoding UTF-8 -source 1.8 -target 1.8 -cp $cp -d $out @(Get-ChildItem "$src\com\mes\plc\*.java").FullName
+```
+
+## Recovery
+
+Delete bad classes under `bin/com/mes/...` then recompile with JDK 8 as above.

BIN
lib/slf4j-api-1.7.36.jar


+ 180 - 0
src/com/mes/plc/QmPlcConfig.java

@@ -0,0 +1,180 @@
+package com.mes.plc;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+/**
+ * 框架气密 OP180A PLC 配置(DB200)。
+ */
+public class QmPlcConfig {
+
+    public final boolean enabled;
+    public String host;
+    public final int rack;
+    public final int slot;
+    public final int db;
+
+    public final int inflationTimeOffset;
+    public final int holdTimeOffset;
+    public final int testTimeOffset;
+    public final int inflationPressureOffset;
+    public final int stablePressureOffset;
+    public final int leakOffset;
+    public final int judgeResultByte;
+    public final int judgeResultBit;
+    public final int frameCodeOffset;
+    public final int frameCodeLen;
+    public final int completeByte;
+    public final int completeBit;
+    public final int allowEntryByte;
+    public final int allowEntryBit;
+    public final int scanFeedbackByte;
+    public final int scanFeedbackBit;
+
+    private QmPlcConfig(Properties pro) {
+        this.enabled = "true".equalsIgnoreCase(trim(pro.getProperty("mes.qm.plc.enabled")));
+        this.host = trim(pro.getProperty("mes.qm.plc.host"));
+        this.rack = parseInt(pro.getProperty("mes.qm.plc.rack"), 0);
+        this.slot = parseInt(pro.getProperty("mes.qm.plc.slot"), 1);
+        this.db = parseInt(pro.getProperty("mes.qm.plc.db"), 200);
+
+        this.inflationTimeOffset = parseInt(pro.getProperty("mes.qm.plc.inflationTimeOffset"), 0);
+        this.holdTimeOffset = parseInt(pro.getProperty("mes.qm.plc.holdTimeOffset"), 4);
+        this.testTimeOffset = parseInt(pro.getProperty("mes.qm.plc.testTimeOffset"), 8);
+        this.inflationPressureOffset = parseInt(pro.getProperty("mes.qm.plc.inflationPressureOffset"), 12);
+        this.stablePressureOffset = parseInt(pro.getProperty("mes.qm.plc.stablePressureOffset"), 16);
+        this.leakOffset = parseInt(pro.getProperty("mes.qm.plc.leakOffset"), 20);
+        this.judgeResultByte = parseInt(pro.getProperty("mes.qm.plc.judgeResultByte"), 24);
+        this.judgeResultBit = parseInt(pro.getProperty("mes.qm.plc.judgeResultBit"), 0);
+        this.frameCodeOffset = parseInt(pro.getProperty("mes.qm.plc.frameCodeOffset"), 26);
+        this.frameCodeLen = parseInt(pro.getProperty("mes.qm.plc.frameCodeLen"), 30);
+        this.completeByte = parseInt(pro.getProperty("mes.qm.plc.completeByte"), 58);
+        this.completeBit = parseInt(pro.getProperty("mes.qm.plc.completeBit"), 0);
+        this.allowEntryByte = parseInt(pro.getProperty("mes.qm.plc.allowEntryByte"), 58);
+        this.allowEntryBit = parseInt(pro.getProperty("mes.qm.plc.allowEntryBit"), 1);
+        this.scanFeedbackByte = parseInt(pro.getProperty("mes.qm.plc.scanFeedbackByte"), 58);
+        this.scanFeedbackBit = parseInt(pro.getProperty("mes.qm.plc.scanFeedbackBit"), 2);
+    }
+
+    public static QmPlcConfig load() {
+        try {
+            return new QmPlcConfig(loadProperties());
+        } catch (Exception e) {
+            e.printStackTrace();
+            return new QmPlcConfig(new Properties());
+        }
+    }
+
+    public static Properties loadProperties() throws Exception {
+        Properties pro = new Properties();
+        File file = resolveConfigFile();
+        if (file != null && file.exists()) {
+            try (InputStream is = new FileInputStream(file);
+                 BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+                pro.load(br);
+            }
+            return pro;
+        }
+        InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+        if (is != null) {
+            try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+                pro.load(br);
+            }
+        }
+        return pro;
+    }
+
+    public static File resolveConfigFile() {
+        try {
+            URL url = ClassLoader.getSystemResource("config/config.properties");
+            if (url != null && "file".equalsIgnoreCase(url.getProtocol())) {
+                return new File(url.toURI());
+            }
+        } catch (Exception ignored) {
+        }
+        File[] candidates = new File[]{
+                new File("src/resources/config/config.properties"),
+                new File("config/config.properties"),
+                new File("bin/config/config.properties")
+        };
+        for (File f : candidates) {
+            if (f.exists()) {
+                return f;
+            }
+        }
+        File dev = new File("src/resources/config/config.properties");
+        if (dev.getParentFile() != null && (dev.getParentFile().exists() || dev.getParentFile().mkdirs())) {
+            return dev;
+        }
+        return new File("config/config.properties");
+    }
+
+    /**
+     * 保存 PLC IP,并尽量写回可写的 config.properties。
+     */
+    public static synchronized boolean saveHost(String newHost) {
+        try {
+            File file = resolveConfigFile();
+            if (file == null) {
+                return false;
+            }
+            if (!file.exists()) {
+                File parent = file.getParentFile();
+                if (parent != null && !parent.exists()) {
+                    parent.mkdirs();
+                }
+            }
+            String content;
+            if (file.exists()) {
+                try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
+                    StringBuilder sb = new StringBuilder();
+                    String line;
+                    boolean replaced = false;
+                    while ((line = br.readLine()) != null) {
+                        if (line.startsWith("mes.qm.plc.host=")) {
+                            sb.append("mes.qm.plc.host=").append(newHost.trim()).append("\n");
+                            replaced = true;
+                        } else {
+                            sb.append(line).append("\n");
+                        }
+                    }
+                    if (!replaced) {
+                        sb.append("mes.qm.plc.host=").append(newHost.trim()).append("\n");
+                    }
+                    content = sb.toString();
+                }
+            } else {
+                content = "mes.qm.plc.host=" + newHost.trim() + "\n";
+            }
+            try (java.io.BufferedWriter bw = new java.io.BufferedWriter(
+                    new java.io.OutputStreamWriter(new java.io.FileOutputStream(file), StandardCharsets.UTF_8))) {
+                bw.write(content);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    private static String trim(String v) {
+        return v == null ? "" : v.trim();
+    }
+
+    private static int parseInt(String v, int def) {
+        try {
+            if (v == null || v.trim().isEmpty()) {
+                return def;
+            }
+            return Integer.parseInt(v.trim());
+        } catch (Exception e) {
+            return def;
+        }
+    }
+}

+ 354 - 0
src/com/mes/plc/QmPlcMonitorPanel.java

@@ -0,0 +1,354 @@
+package com.mes.plc;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import java.awt.*;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 框架气密 DB200 调试:读取 mes 数据块全部点位。
+ */
+public class QmPlcMonitorPanel extends JPanel {
+
+    private final JLabel statusLabel = new JLabel("未连接");
+    private final JTextField hostField = new JTextField(16);
+    private final JLabel dbLabel = new JLabel("-");
+
+    private final PointRow inflationTime = new PointRow("框架充气时间", "Real");
+    private final PointRow holdTime = new PointRow("框架保压时间", "Real");
+    private final PointRow testTime = new PointRow("框架测试时间", "Real");
+    private final PointRow inflationPressure = new PointRow("框架充气压力", "Real");
+    private final PointRow stablePressure = new PointRow("框架稳定压力", "Real");
+    private final PointRow leak = new PointRow("框架泄漏量", "Real");
+    private final PointRow judgeResult = new PointRow("框架判定结果", "Bool");
+    private final PointRow frameCode = new PointRow("框架码", "String[30]");
+    private final PointRow complete = new PointRow("检测完成", "Bool");
+    private final PointRow allowEntry = new PointRow("允许进站", "Bool");
+    private final PointRow scanFeedback = new PointRow("扫码反馈", "Bool");
+
+    private QmPlcConfig config;
+    private QmS7Client s7Client;
+    private Timer timer;
+    private final AtomicBoolean refreshing = new AtomicBoolean(false);
+    private volatile boolean active;
+
+    public QmPlcMonitorPanel() {
+        setLayout(new BorderLayout(10, 10));
+        setBorder(new EmptyBorder(12, 16, 12, 16));
+
+        JPanel top = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 8));
+        JLabel ipTitle = new JLabel("PLC IP:");
+        ipTitle.setFont(new Font("微软雅黑", Font.PLAIN, 18));
+        hostField.setFont(new Font("Consolas", Font.PLAIN, 18));
+        hostField.setPreferredSize(new Dimension(180, 32));
+
+        JButton saveIpBtn = new JButton("保存IP");
+        saveIpBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        saveIpBtn.addActionListener(e -> saveIp());
+
+        JButton refreshBtn = new JButton("立即刷新");
+        refreshBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        refreshBtn.addActionListener(e -> refreshOnce());
+
+        statusLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
+        dbLabel.setFont(new Font("Consolas", Font.PLAIN, 16));
+
+        top.add(ipTitle);
+        top.add(hostField);
+        top.add(saveIpBtn);
+        top.add(refreshBtn);
+        top.add(dbLabel);
+        top.add(statusLabel);
+        add(top, BorderLayout.NORTH);
+
+        JPanel center = new JPanel();
+        center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS));
+        center.setBorder(new TitledBorder("mes [DB200] 点位"));
+        center.add(headerRow());
+        center.add(inflationTime);
+        center.add(holdTime);
+        center.add(testTime);
+        center.add(inflationPressure);
+        center.add(stablePressure);
+        center.add(leak);
+        center.add(judgeResult);
+        center.add(frameCode);
+        center.add(complete);
+        center.add(allowEntry);
+        center.add(scanFeedback);
+
+        JScrollPane scroll = new JScrollPane(center);
+        scroll.setBorder(null);
+        add(scroll, BorderLayout.CENTER);
+
+        loadUiFromConfig();
+    }
+
+    private JPanel headerRow() {
+        JPanel p = new JPanel(new GridLayout(1, 4, 8, 0));
+        p.setMaximumSize(new Dimension(Integer.MAX_VALUE, 28));
+        p.add(labelBold("名称"));
+        p.add(labelBold("数据类型"));
+        p.add(labelBold("偏移量"));
+        p.add(labelBold("当前值"));
+        return p;
+    }
+
+    private static JLabel labelBold(String text) {
+        JLabel l = new JLabel(text);
+        l.setFont(new Font("微软雅黑", Font.BOLD, 15));
+        return l;
+    }
+
+    private void loadUiFromConfig() {
+        config = QmPlcConfig.load();
+        hostField.setText(config.host);
+        dbLabel.setText("DB" + config.db);
+        inflationTime.setAddr(String.format("DB%d.DBD%d", config.db, config.inflationTimeOffset));
+        holdTime.setAddr(String.format("DB%d.DBD%d", config.db, config.holdTimeOffset));
+        testTime.setAddr(String.format("DB%d.DBD%d", config.db, config.testTimeOffset));
+        inflationPressure.setAddr(String.format("DB%d.DBD%d", config.db, config.inflationPressureOffset));
+        stablePressure.setAddr(String.format("DB%d.DBD%d", config.db, config.stablePressureOffset));
+        leak.setAddr(String.format("DB%d.DBD%d", config.db, config.leakOffset));
+        judgeResult.setAddr(String.format("DB%d.DBX%d.%d", config.db, config.judgeResultByte, config.judgeResultBit));
+        frameCode.setAddr(String.format("DB%d.DBB%d String[%d]", config.db, config.frameCodeOffset, config.frameCodeLen));
+        complete.setAddr(String.format("DB%d.DBX%d.%d", config.db, config.completeByte, config.completeBit));
+        allowEntry.setAddr(String.format("DB%d.DBX%d.%d", config.db, config.allowEntryByte, config.allowEntryBit));
+        scanFeedback.setAddr(String.format("DB%d.DBX%d.%d", config.db, config.scanFeedbackByte, config.scanFeedbackBit));
+    }
+
+    private void saveIp() {
+        String ip = hostField.getText() == null ? "" : hostField.getText().trim();
+        if (ip.isEmpty()) {
+            JOptionPane.showMessageDialog(this, "请输入 PLC IP", "提示", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+        boolean ok = QmPlcConfig.saveHost(ip);
+        if (config != null) {
+            config.host = ip;
+        }
+        if (s7Client != null) {
+            s7Client.updateHost(ip);
+        }
+        QmPlcPoller.reloadHost(ip);
+        if (ok) {
+            statusLabel.setForeground(new Color(0, 128, 0));
+            statusLabel.setText("IP已保存: " + ip);
+        } else {
+            statusLabel.setForeground(Color.ORANGE.darker());
+            statusLabel.setText("IP已用于当前连接,但写配置文件失败");
+        }
+        refreshOnce();
+    }
+
+    public void startMonitor() {
+        active = true;
+        loadUiFromConfig();
+        if (s7Client == null) {
+            s7Client = new QmS7Client(config);
+        } else {
+            s7Client.updateHost(hostField.getText().trim());
+        }
+        if (timer != null) {
+            timer.cancel();
+        }
+        timer = new Timer("QmPlcMonitor", true);
+        timer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                if (active) {
+                    refreshOnce();
+                }
+            }
+        }, 200, 1000);
+    }
+
+    public void stopMonitor() {
+        active = false;
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
+        }
+        if (s7Client != null) {
+            s7Client.close();
+            s7Client = null;
+        }
+        SwingUtilities.invokeLater(() -> statusLabel.setText("已停止刷新"));
+    }
+
+    private void refreshOnce() {
+        if (!refreshing.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            if (config == null) {
+                config = QmPlcConfig.load();
+            }
+            String ip = hostField.getText() == null ? "" : hostField.getText().trim();
+            if (!ip.isEmpty()) {
+                config.host = ip;
+            }
+            if (s7Client == null) {
+                s7Client = new QmS7Client(config);
+            } else {
+                s7Client.updateHost(config.host);
+            }
+
+            final Snapshot s = new Snapshot();
+            s.inflationTime = readFloat(config.inflationTimeOffset, s);
+            s.holdTime = readFloat(config.holdTimeOffset, s);
+            s.testTime = readFloat(config.testTimeOffset, s);
+            s.inflationPressure = readFloat(config.inflationPressureOffset, s);
+            s.stablePressure = readFloat(config.stablePressureOffset, s);
+            s.leak = readFloat(config.leakOffset, s);
+            s.judgeResult = readBit(config.judgeResultByte, config.judgeResultBit, s);
+            s.frameCode = readFrame(s);
+            s.complete = readBit(config.completeByte, config.completeBit, s);
+            s.allowEntry = readBit(config.allowEntryByte, config.allowEntryBit, s);
+            s.scanFeedback = readBit(config.scanFeedbackByte, config.scanFeedbackBit, s);
+
+            final boolean allOk = s.errorCount == 0;
+            SwingUtilities.invokeLater(() -> {
+                inflationTime.setValue(s.inflationTime);
+                holdTime.setValue(s.holdTime);
+                testTime.setValue(s.testTime);
+                inflationPressure.setValue(s.inflationPressure);
+                stablePressure.setValue(s.stablePressure);
+                leak.setValue(s.leak);
+                judgeResult.setValue(s.judgeResult);
+                frameCode.setValue(s.frameCode);
+                complete.setValue(s.complete);
+                allowEntry.setValue(s.allowEntry);
+                scanFeedback.setValue(s.scanFeedback);
+                if (allOk) {
+                    statusLabel.setForeground(new Color(0, 128, 0));
+                    statusLabel.setText("已连接 · 实时刷新中");
+                } else {
+                    statusLabel.setForeground(Color.RED);
+                    statusLabel.setText("部分读取失败: " + shorten(s.firstError));
+                }
+            });
+
+            if (s.errorCount >= 11 && s7Client != null) {
+                s7Client.close();
+                s7Client = null;
+            }
+        } catch (Exception e) {
+            if (s7Client != null) {
+                s7Client.close();
+                s7Client = null;
+            }
+            final String msg = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
+            SwingUtilities.invokeLater(() -> {
+                statusLabel.setForeground(Color.RED);
+                statusLabel.setText("读取失败: " + shorten(msg));
+            });
+        } finally {
+            refreshing.set(false);
+        }
+    }
+
+    private String readFloat(int offset, Snapshot s) {
+        try {
+            return String.format("%.3f", s7Client.readFloat(offset));
+        } catch (Exception e) {
+            s.errorCount++;
+            if (s.firstError == null) {
+                s.firstError = e.getMessage();
+            }
+            return "ERR";
+        }
+    }
+
+    private String readBit(int byteOffset, int bit, Snapshot s) {
+        try {
+            return s7Client.readBit(byteOffset, bit) ? "true" : "false";
+        } catch (Exception e) {
+            s.errorCount++;
+            if (s.firstError == null) {
+                s.firstError = e.getMessage();
+            }
+            return "ERR";
+        }
+    }
+
+    private String readFrame(Snapshot s) {
+        try {
+            String v = s7Client.readFrameCode();
+            return (v == null || v.isEmpty()) ? "(空)" : v;
+        } catch (Exception e) {
+            s.errorCount++;
+            if (s.firstError == null) {
+                s.firstError = e.getMessage();
+            }
+            return "ERR";
+        }
+    }
+
+    private static String shorten(String msg) {
+        if (msg == null) {
+            return "";
+        }
+        String m = msg.replace('\n', ' ').trim();
+        if (m.length() > 120) {
+            return m.substring(0, 120) + "...";
+        }
+        return m;
+    }
+
+    private static class Snapshot {
+        String inflationTime;
+        String holdTime;
+        String testTime;
+        String inflationPressure;
+        String stablePressure;
+        String leak;
+        String judgeResult;
+        String frameCode;
+        String complete;
+        String allowEntry;
+        String scanFeedback;
+        int errorCount;
+        String firstError;
+    }
+
+    private static class PointRow extends JPanel {
+        private final JLabel addrLabel = new JLabel("-");
+        private final JLabel valueLabel = new JLabel("-");
+
+        PointRow(String name, String type) {
+            setLayout(new GridLayout(1, 4, 8, 0));
+            setMaximumSize(new Dimension(Integer.MAX_VALUE, 36));
+            JLabel nameLabel = new JLabel(name);
+            nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+            JLabel typeLabel = new JLabel(type);
+            typeLabel.setFont(new Font("Consolas", Font.PLAIN, 14));
+            addrLabel.setFont(new Font("Consolas", Font.PLAIN, 14));
+            valueLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
+            add(nameLabel);
+            add(typeLabel);
+            add(addrLabel);
+            add(valueLabel);
+        }
+
+        void setAddr(String addr) {
+            addrLabel.setText(addr);
+        }
+
+        void setValue(String value) {
+            valueLabel.setText(value == null ? "-" : value);
+            if ("ERR".equals(value)) {
+                valueLabel.setForeground(Color.RED);
+            } else if ("true".equals(value)) {
+                valueLabel.setForeground(new Color(0, 128, 0));
+            } else if ("false".equals(value)) {
+                valueLabel.setForeground(Color.GRAY);
+            } else {
+                valueLabel.setForeground(Color.BLACK);
+            }
+        }
+    }
+}

+ 306 - 0
src/com/mes/plc/QmPlcPoller.java

@@ -0,0 +1,306 @@
+package com.mes.plc;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.ui.DataUtil;
+import com.mes.ui.MesClient;
+import com.mes.util.DateLocalUtils;
+import com.mes.util.JdbcUtils;
+import com.mes.util.TestParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 框架气密 PLC 定时轮询:扫码进站 + 检测完成上传。
+ */
+public class QmPlcPoller {
+
+    private static final Logger log = LoggerFactory.getLogger(QmPlcPoller.class);
+
+    private static Timer timer;
+    private static final AtomicBoolean running = new AtomicBoolean(false);
+
+    private static QmPlcConfig config;
+    private static QmS7Client s7Client;
+
+    private static boolean lastComplete;
+    private static boolean handshakeBusy;
+    private static boolean uploadBusy;
+    private static String currentSn = "";
+    private static boolean entered;
+    private static String lastCheckedSn = "";
+    private static String lastUploadedSn = "";
+
+    public static void start() {
+        config = QmPlcConfig.load();
+        if (!config.enabled) {
+            log.info("框架气密PLC轮询未启用 mes.qm.plc.enabled=false");
+            MesClient.qmPlcEnabled = false;
+            return;
+        }
+        s7Client = new QmS7Client(config);
+        MesClient.qmPlcEnabled = true;
+        SwingUtilities.invokeLater(MesClient::applyPlcWorkUi);
+
+        if (timer != null) {
+            timer.cancel();
+        }
+        timer = new Timer("QmPlcPoller", true);
+        timer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                pollOnce();
+            }
+        }, 1000, 1000);
+        log.info("框架气密PLC轮询已启动 host={} DB{}", config.host, config.db);
+        SwingUtilities.invokeLater(() -> MesClient.setMenuStatus("等待PLC扫码", 0));
+    }
+
+    /** 调试页改 IP 后刷新轮询连接 */
+    public static void reloadHost(String host) {
+        if (config != null && s7Client != null) {
+            s7Client.updateHost(host);
+        }
+    }
+
+    public static void stop() {
+        if (timer != null) {
+            timer.cancel();
+            timer = null;
+        }
+        if (s7Client != null) {
+            s7Client.close();
+        }
+        MesClient.qmPlcEnabled = false;
+    }
+
+    public static void resetCycle() {
+        currentSn = "";
+        entered = false;
+        lastComplete = false;
+        handshakeBusy = false;
+        uploadBusy = false;
+        lastCheckedSn = "";
+        // lastUploadedSn 保留,防止清完成位失败后重复上传;新进站时覆盖
+    }
+
+    private static void pollOnce() {
+        if (!running.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            handleScanFeedback();
+            handleComplete();
+        } catch (Exception e) {
+            log.error("框架气密PLC轮询异常: {}", e.getMessage());
+            if (s7Client != null) {
+                s7Client.close();
+            }
+        } finally {
+            running.set(false);
+        }
+    }
+
+    private static void handleScanFeedback() throws Exception {
+        if (handshakeBusy || entered || MesClient.work_status == 1) {
+            return;
+        }
+        boolean feedback = s7Client.readBit(config.scanFeedbackByte, config.scanFeedbackBit);
+        if (!feedback) {
+            return;
+        }
+
+        handshakeBusy = true;
+        try {
+            String sn = s7Client.readFrameCode();
+            if (sn == null || sn.isEmpty()) {
+                updateStatus("PLC框架码为空,保持扫码反馈待重试", false);
+                return;
+            }
+            if (sn.equals(lastCheckedSn)) {
+                return;
+            }
+
+            JSONObject resp = DataUtil.checkQualityHttp(sn);
+            if (resp == null) {
+                lastCheckedSn = sn;
+                s7Client.clearScanFeedback();
+                updateStatus("质量校验请求失败: " + sn, false);
+                return;
+            }
+            boolean pass = "true".equalsIgnoreCase(String.valueOf(resp.get("result")));
+            lastCheckedSn = sn;
+            if (pass) {
+                s7Client.allowEntryAndClearScanFeedback();
+                currentSn = sn;
+                entered = true;
+                lastComplete = s7Client.readBit(config.completeByte, config.completeBit);
+                SwingUtilities.invokeLater(() -> {
+                    MesClient.product_sn.setText(sn);
+                    MesClient.product_sn.setEditable(false);
+                    MesClient.check_quality_result = true;
+                    MesClient.work_status = 1;
+                    MesClient.tjFlag = 1;
+                    if (MesClient.result != null) {
+                        MesClient.result.setText("等待结果");
+                        MesClient.result.setForeground(Color.GRAY);
+                    }
+                });
+                updateStatus("允许进站: " + sn + ",等待检测完成", true);
+            } else {
+                s7Client.clearScanFeedback();
+                String msg = resp.getString("message");
+                if (msg == null || msg.isEmpty()) {
+                    msg = "不可加工";
+                }
+                updateStatus(msg + " [" + sn + "]", false);
+            }
+        } finally {
+            handshakeBusy = false;
+        }
+    }
+
+    private static void handleComplete() throws Exception {
+        if (!entered || currentSn == null || currentSn.isEmpty()) {
+            lastComplete = false;
+            return;
+        }
+        if (uploadBusy) {
+            return;
+        }
+
+        boolean complete = s7Client.readBit(config.completeByte, config.completeBit);
+        // 保持完成位为 true 时持续尝试上传(失败可重试);首次从 false→true 或已完成待上传均可
+        if (!complete) {
+            lastComplete = false;
+            return;
+        }
+        lastComplete = true;
+
+        uploadBusy = true;
+        try {
+            String sn = s7Client.readFrameCode();
+            if (sn == null || sn.isEmpty()) {
+                sn = currentSn;
+            }
+            if (sn == null || sn.isEmpty()) {
+                log.error("框架气密采集失败:无框架码,保持检测完成待重试");
+                return;
+            }
+
+            // 已上传成功但清完成位失败时,只重试清位,避免重复提交
+            if (sn.equals(lastUploadedSn)) {
+                s7Client.clearComplete();
+                SwingUtilities.invokeLater(() -> {
+                    MesClient.resetScanA();
+                    MesClient.setMenuStatus("测试结果上传成功,请扫下一件", 0);
+                });
+                resetCycle();
+                return;
+            }
+
+            float inflationTime = s7Client.readFloat(config.inflationTimeOffset);
+            float holdTime = s7Client.readFloat(config.holdTimeOffset);
+            float testTime = s7Client.readFloat(config.testTimeOffset);
+            float inflationPressure = s7Client.readFloat(config.inflationPressureOffset);
+            float stablePressure = s7Client.readFloat(config.stablePressureOffset);
+            float leak = s7Client.readFloat(config.leakOffset);
+            boolean judgePass = s7Client.readBit(config.judgeResultByte, config.judgeResultBit);
+            String stationResult = judgePass ? "OK" : "NG";
+
+            log.info("框架气密采集 sn={} 充气时间={} 保压时间={} 测试时间={} 充气压力={} 稳定压力={} 泄漏量={} 判定={}",
+                    sn, inflationTime, holdTime, testTime, inflationPressure, stablePressure, leak, stationResult);
+
+            final String snF = sn;
+            final String resultF = stationResult;
+            final float stableF = stablePressure;
+            final float leakF = leak;
+            final float cqF = inflationTime;
+            final float byF = holdTime;
+            final float csF = testTime;
+            final float inflationPressureF = inflationPressure;
+
+            TestParam testParam = buildTestParam(snF, resultF, stableF, leakF, cqF, byF, csF, inflationPressureF);
+            JSONObject resp = DataUtil.qmResultData(testParam);
+            boolean ok = resp != null && "true".equalsIgnoreCase(String.valueOf(resp.get("result")));
+            if (!ok) {
+                log.error("框架气密结果上传失败,保持检测完成待重试 sn={}", snF);
+                updateStatus("测试结果上传失败,请重试 [" + snF + "]", false);
+                return;
+            }
+
+            lastUploadedSn = snF;
+            s7Client.clearComplete();
+            try {
+                JdbcUtils.insertTestRecord(testParam);
+            } catch (Exception e) {
+                log.warn("本地测试记录保存失败: {}", e.getMessage());
+            }
+
+            SwingUtilities.invokeLater(() -> {
+                if (MesClient.param1 != null) {
+                    MesClient.param1.setText(fmt(stableF));
+                }
+                if (MesClient.param2 != null) {
+                    MesClient.param2.setText(fmt(leakF));
+                }
+                if (MesClient.param3 != null) {
+                    MesClient.param3.setText(fmt(byF));
+                }
+                if (MesClient.param4 != null) {
+                    MesClient.param4.setText(fmt(csF));
+                }
+                if (MesClient.result != null) {
+                    MesClient.result.setText(resultF);
+                    MesClient.result.setForeground("OK".equals(resultF) ? Color.GREEN : Color.RED);
+                }
+                MesClient.resetScanA();
+                MesClient.setMenuStatus("测试结果上传成功,请扫下一件", 0);
+            });
+            resetCycle();
+            log.info("框架气密采集完成 sn={} result={}", snF, resultF);
+        } finally {
+            uploadBusy = false;
+        }
+    }
+
+    private static TestParam buildTestParam(String sn, String result, float stablePressure, float leak,
+                                           float cq, float by, float cs, float inflationPressure) {
+        TestParam testParam = new TestParam();
+        testParam.setSn(sn);
+        testParam.setResult(result);
+        testParam.setParam1(fmt(stablePressure));
+        testParam.setParam2("kPa");
+        testParam.setParam3(fmt(leak));
+        testParam.setParam4("ml/min");
+        testParam.setParam5("");
+        testParam.setFillDuration(fmt(cq));
+        testParam.setStabilizeTime(fmt(by));
+        testParam.setTestDuration(fmt(cs));
+        testParam.setRemark("充气压力=" + fmt(inflationPressure));
+        testParam.setDeviceType("plc");
+        testParam.setCreateTime(DateLocalUtils.getCurrentDate() + " " + DateLocalUtils.getCurrentTimeHMS());
+        testParam.setUcode(MesClient.user_menu != null ? MesClient.user_menu.getText() : "");
+        testParam.setOprno(MesClient.mes_gw != null && !MesClient.mes_gw.isEmpty()
+                ? MesClient.mes_gw
+                : (MesClient.configParam != null ? MesClient.configParam.getOprno() : "OP180A"));
+        testParam.setLineSn(MesClient.mes_line_sn != null && !MesClient.mes_line_sn.isEmpty()
+                ? MesClient.mes_line_sn
+                : (MesClient.configParam != null ? MesClient.configParam.getLineSn() : "XT"));
+        testParam.setOprnoTitle("框架气密");
+        return testParam;
+    }
+
+    private static String fmt(float v) {
+        return String.format("%.3f", v);
+    }
+
+    private static void updateStatus(final String msg, final boolean ok) {
+        SwingUtilities.invokeLater(() -> MesClient.setMenuStatus(msg, ok ? 0 : -1));
+    }
+}

+ 138 - 0
src/com/mes/plc/QmS7Client.java

@@ -0,0 +1,138 @@
+package com.mes.plc;
+
+import com.github.s7connector.api.DaveArea;
+import com.github.s7connector.api.S7Connector;
+import com.github.s7connector.api.factory.S7ConnectorFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 框架气密工位 S7 读写封装(DB200)。
+ */
+public class QmS7Client {
+
+    private final QmPlcConfig config;
+    private volatile S7Connector connector;
+
+    public QmS7Client(QmPlcConfig config) {
+        this.config = config;
+    }
+
+    public void updateHost(String host) {
+        if (host != null && !host.equals(config.host)) {
+            config.host = host.trim();
+            close();
+        }
+    }
+
+    public synchronized void ensureConnected() {
+        if (connector != null) {
+            return;
+        }
+        if (config.host == null || config.host.isEmpty() || config.host.contains("xxx")) {
+            throw new IllegalStateException("未配置有效的 mes.qm.plc.host");
+        }
+        connector = S7ConnectorFactory
+                .buildTCPConnector()
+                .withHost(config.host)
+                .withRack(config.rack)
+                .withSlot(config.slot)
+                .withTimeout(3000)
+                .build();
+        System.out.println("框架气密PLC已连接: " + config.host + " DB" + config.db);
+    }
+
+    public synchronized void close() {
+        if (connector != null) {
+            try {
+                connector.close();
+            } catch (Exception ignored) {
+            }
+            connector = null;
+        }
+    }
+
+    public boolean readBit(int byteOffset, int bit) throws Exception {
+        ensureConnected();
+        byte[] data = connector.read(DaveArea.DB, config.db, 1, byteOffset);
+        return getBit(data[0], bit);
+    }
+
+    public void writeBit(int byteOffset, int bit, boolean value) throws Exception {
+        ensureConnected();
+        byte[] data = connector.read(DaveArea.DB, config.db, 1, byteOffset);
+        data[0] = setBit(data[0], bit, value);
+        connector.write(DaveArea.DB, config.db, byteOffset, data);
+    }
+
+    /**
+     * 校验通过:写允许进站=true,同时清扫码反馈=false(同字节一次写入)。
+     */
+    public void allowEntryAndClearScanFeedback() throws Exception {
+        ensureConnected();
+        if (config.allowEntryByte == config.scanFeedbackByte) {
+            byte[] data = connector.read(DaveArea.DB, config.db, 1, config.allowEntryByte);
+            data[0] = setBit(data[0], config.allowEntryBit, true);
+            data[0] = setBit(data[0], config.scanFeedbackBit, false);
+            connector.write(DaveArea.DB, config.db, config.allowEntryByte, data);
+        } else {
+            writeBit(config.allowEntryByte, config.allowEntryBit, true);
+            writeBit(config.scanFeedbackByte, config.scanFeedbackBit, false);
+        }
+    }
+
+    public void clearScanFeedback() throws Exception {
+        writeBit(config.scanFeedbackByte, config.scanFeedbackBit, false);
+    }
+
+    public void clearComplete() throws Exception {
+        writeBit(config.completeByte, config.completeBit, false);
+    }
+
+    public String readFrameCode() throws Exception {
+        ensureConnected();
+        int readLen = config.frameCodeLen + 2;
+        byte[] data = connector.read(DaveArea.DB, config.db, readLen, config.frameCodeOffset);
+        int curLen = data[1] & 0xFF;
+        if (curLen < 0) {
+            curLen = 0;
+        }
+        if (curLen > config.frameCodeLen) {
+            curLen = config.frameCodeLen;
+        }
+        String sn = new String(data, 2, curLen, StandardCharsets.US_ASCII).trim();
+        if (sn.isEmpty()) {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 2; i < data.length; i++) {
+                char c = (char) (data[i] & 0xFF);
+                if (c == 0) {
+                    break;
+                }
+                if (c >= 32 && c < 127) {
+                    sb.append(c);
+                }
+            }
+            sn = sb.toString().trim();
+        }
+        return sn;
+    }
+
+    public float readFloat(int offset) throws Exception {
+        ensureConnected();
+        byte[] data = connector.read(DaveArea.DB, config.db, 4, offset);
+        return ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN).getFloat();
+    }
+
+    public static boolean getBit(byte b, int n) {
+        return ((b >> n) & 1) == 1;
+    }
+
+    public static byte setBit(byte b, int n, boolean value) {
+        if (value) {
+            return (byte) (b | (1 << n));
+        }
+        return (byte) (b & ~(1 << n));
+    }
+}

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

@@ -276,8 +276,8 @@ public class DataUtil {
                     +"&title="+URLEncoder.encode(titleBase64, "UTF-8")
                     +"&remark="+URLEncoder.encode(testParam.getRemark() != null ? testParam.getRemark() : "", "UTF-8")
                     +"&deviceType="+URLEncoder.encode(testParam.getDeviceType() != null ? testParam.getDeviceType() : "", "UTF-8")
-                    // cq=充 by=稳压时长 cs=测试时长(秒);testTime 仍为完成时刻
-                    +"&cq="+URLEncoder.encode("", "UTF-8")
+                    // cq=充气时长 by=稳压时长 cs=测试时长(秒);testTime 仍为完成时刻
+                    +"&cq="+URLEncoder.encode(testParam.getFillDuration() != null ? testParam.getFillDuration() : "", "UTF-8")
                     +"&by="+URLEncoder.encode(testParam.getStabilizeTime() != null ? testParam.getStabilizeTime() : "", "UTF-8")
                     +"&cs="+URLEncoder.encode(testParam.getTestDuration() != null ? testParam.getTestDuration() : "", "UTF-8")
                     +"&leakRate="+URLEncoder.encode("", "UTF-8")
@@ -288,7 +288,8 @@ public class DataUtil {
 //            if (MesClient.sessionid != null && !MesClient.sessionid.isEmpty()) {
 //                params += "&__sid=" + MesClient.sessionid;
 //            }
-            log.info("提交时长字段 by(稳压时长)={}, cs(测试时长)={}", testParam.getStabilizeTime(), testParam.getTestDuration());
+            log.info("提交时长字段 cq(充气)={}, by(稳压)={}, cs(测试)={}",
+                    testParam.getFillDuration(), testParam.getStabilizeTime(), testParam.getTestDuration());
             log.info("params="+params);
             String result = doPost(url,params);
             log.info("result="+result);
@@ -318,6 +319,43 @@ public class DataUtil {
         }
     }
 
+    /**
+     * HTTP 同步质量校验(PLC 进站用)
+     */
+    public static JSONObject checkQualityHttp(String sn) {
+        try {
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+            pro.load(br);
+            br.close();
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String oprno = MesClient.mes_gw != null && !MesClient.mes_gw.isEmpty()
+                    ? MesClient.mes_gw.trim()
+                    : pro.getProperty("mes.gw").trim();
+            String lineSn = MesClient.mes_line_sn != null && !MesClient.mes_line_sn.isEmpty()
+                    ? MesClient.mes_line_sn.trim()
+                    : pro.getProperty("mes.line_sn").trim();
+            String userCode = MesClient.user_menu != null ? MesClient.user_menu.getText() : "";
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductRecord/checkQuality";
+            String params = "__ajax=json&sn=" + URLEncoder.encode(sn.trim(), "UTF-8")
+                    + "&oprno=" + URLEncoder.encode(oprno, "UTF-8")
+                    + "&lineSn=" + URLEncoder.encode(lineSn, "UTF-8")
+                    + "&userCode=" + URLEncoder.encode(userCode, "UTF-8");
+            log.info("checkQualityHttp params={}", params);
+            String result = doPost(url, params);
+            log.info("checkQualityHttp result={}", result);
+            if (result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            return JSONObject.parseObject(result);
+        } catch (Exception e) {
+            log.error("checkQualityHttp异常: {}", e.getMessage());
+            e.printStackTrace();
+            return null;
+        }
+    }
+
     public static String doPost(String httpUrl, String param) {
         HttpURLConnection connection = null;
         InputStream is = null;

+ 3 - 0
src/com/mes/ui/LoginFarme.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.mes.plc.QmPlcPoller;
 import com.mes.util.Base64Utils;
 import com.mes.util.HttpUtils;
 
@@ -176,6 +177,8 @@ public class LoginFarme extends JFrame {
                     MesClient.initTcpConnection();
                     //启动timer心跳包
                     MesClient.startHeartBeatTimer();
+                    // 启动框架气密 PLC 轮询(扫码进站 + 检测完成上传)
+                    QmPlcPoller.start();
 
                     //1操作工人,2管理员
                     //登录成功

+ 50 - 80
src/com/mes/ui/MesClient.java

@@ -4,6 +4,8 @@ import com.alibaba.fastjson2.JSONObject;
 import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
 import com.mes.netty.NettyClient;
+import com.mes.plc.QmPlcMonitorPanel;
+import com.mes.plc.QmPlcPoller;
 import com.mes.util.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -77,6 +79,8 @@ public class MesClient extends JFrame {
 
     public static String user20 = "";
     public static boolean mes_enable = true; // true=MES开启
+    /** PLC 模式启用时,进站与结果由 QmPlcPoller 驱动,串口/WorkTimer 不参与上传 */
+    public static boolean qmPlcEnabled = false;
 
     public static JFrame welcomeWin;
 
@@ -85,6 +89,7 @@ public class MesClient extends JFrame {
     public static JPanel indexPanelC;
     public static JPanel indexPanelD;
     public static JPanel indexPanelTest;
+    public static QmPlcMonitorPanel qmPlcMonitorPanel;
     public static JTextField testPressureField;
     public static JTextField testLeakField;
     public static JTextField testStatusField;
@@ -341,10 +346,12 @@ public class MesClient extends JFrame {
         MesClient.finish_ok_bt.setEnabled(false);
         MesClient.finish_ng_bt.setEnabled(false);
         product_sn.setText("");
-        product_sn.setEditable(true);
+        // PLC 模式下工件码只读显示框架码;非 PLC 模式允许手动扫码
+        product_sn.setEditable(!qmPlcEnabled);
         tjFlag = 0;
         mesStartFlag = 0;
         WorkTimer.resetEndConfirm();
+        QmPlcPoller.resetCycle();
         curTaskName.setText("");
         curStabilizeTime = "";
         curTestDuration = "";
@@ -358,7 +365,11 @@ public class MesClient extends JFrame {
 //        f_scan_data_bt_1.setIcon(new ImageIcon(MesClient.class.getResource("/bg/scan_barcode.png")));
 //        f_scan_data_bt_1.setText("扫码");
 //        MesClient.f_scan_data_bt_1.setEnabled(true);
-        MesClient.setMenuStatus("请扫工件码",0);
+        if (qmPlcEnabled) {
+            MesClient.setMenuStatus("等待PLC扫码", 0);
+        } else {
+            MesClient.setMenuStatus("请扫工件码", 0);
+        }
 
 //        MesClient.result.setText("等待结果");
 //        MesClient.result.setForeground(Color.GRAY);
@@ -685,6 +696,10 @@ public class MesClient extends JFrame {
     }
 
     public static void scanBarcode2() {
+        if (qmPlcEnabled) {
+            MesClient.setMenuStatus("PLC模式:等待设备扫码", 0);
+            return;
+        }
         if(work_status == 1){
 //            MesClient.serialPortUtils.sendData("START");
             return;
@@ -1001,13 +1016,16 @@ public class MesClient extends JFrame {
 
         product_sn = new JTextField();
         product_sn.setHorizontalAlignment(SwingConstants.LEFT);
-        product_sn.setEditable(true);
+        product_sn.setEditable(false);
         product_sn.setFont(new Font("微软雅黑", Font.PLAIN, 28));
-        product_sn.setBounds(81, 70, 602, 70);
+        product_sn.setBounds(81, 70, 810, 70);
         indexPanelA.add(product_sn);
         product_sn.setColumns(10);
         product_sn.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent e) {
+                if (qmPlcEnabled) {
+                    return;
+                }
                 scan_type = 1;
                 scanBarcode2();
             }
@@ -1024,7 +1042,8 @@ public class MesClient extends JFrame {
 //        f_scan_data_bt_1.setIcon(new ImageIcon(MesClient.class.getResource("/bg/scan_barcode.png")));
         f_scan_data_bt_1.setFont(new Font("微软雅黑", Font.PLAIN, 32));
         f_scan_data_bt_1.setBounds(693, 70, 198, 70);
-        indexPanelA.add(f_scan_data_bt_1);
+        f_scan_data_bt_1.setVisible(false);
+//        indexPanelA.add(f_scan_data_bt_1);
         
         JLabel lblNewLabel = new JLabel("测试压");
         lblNewLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
@@ -1693,76 +1712,9 @@ public class MesClient extends JFrame {
 
         tabbedPane.addTab("软件设置", new ImageIcon(MesClient.class.getResource("/bg/menu_setting.png")), searchScrollPaneD, null);
 
-        // 通讯测试页:手动发送读取实时压力/泄漏
-        indexPanelTest = new JPanel();
-        JScrollPane searchScrollPaneTest = new JScrollPane(indexPanelTest);
-        indexPanelTest.setLayout(null);
-
-        JLabel testTitle = new JLabel("气密仪实时数据读取(点击发送读取压力/泄漏)");
-        testTitle.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testTitle.setBounds(80, 30, 700, 40);
-        indexPanelTest.add(testTitle);
-
-        JLabel testPressureLabel = new JLabel("测试压");
-        testPressureLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-        testPressureLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testPressureLabel.setBounds(80, 100, 120, 40);
-        indexPanelTest.add(testPressureLabel);
-
-        testPressureField = new JTextField();
-        testPressureField.setEditable(false);
-        testPressureField.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testPressureField.setBounds(220, 100, 280, 40);
-        indexPanelTest.add(testPressureField);
-
-        JLabel testLeakLabel = new JLabel("泄露值");
-        testLeakLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-        testLeakLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testLeakLabel.setBounds(80, 160, 120, 40);
-        indexPanelTest.add(testLeakLabel);
-
-        testLeakField = new JTextField();
-        testLeakField.setEditable(false);
-        testLeakField.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testLeakField.setBounds(220, 160, 280, 40);
-        indexPanelTest.add(testLeakField);
-
-        JLabel testStatusLabel = new JLabel("状态码");
-        testStatusLabel.setHorizontalAlignment(SwingConstants.RIGHT);
-        testStatusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testStatusLabel.setBounds(80, 220, 120, 40);
-        indexPanelTest.add(testStatusLabel);
-
-        testStatusField = new JTextField();
-        testStatusField.setEditable(false);
-        testStatusField.setFont(new Font("微软雅黑", Font.PLAIN, 20));
-        testStatusField.setBounds(220, 220, 280, 40);
-        indexPanelTest.add(testStatusField);
-
-        testSendBtn = new JButton("发送");
-        testSendBtn.setFont(new Font("微软雅黑", Font.PLAIN, 28));
-        testSendBtn.setBounds(540, 100, 160, 100);
-        testSendBtn.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-                sendDeviceTestRead();
-            }
-        });
-        indexPanelTest.add(testSendBtn);
-
-        JLabel testTipLabel = new JLabel("说明:泄漏=先发 10 30 04,再发 03 22 0A 00 02;设备需为 ateq");
-        testTipLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
-        testTipLabel.setBounds(80, 280, 700, 30);
-        indexPanelTest.add(testTipLabel);
-
-        testLogArea = new JTextArea();
-        testLogArea.setEditable(false);
-        testLogArea.setFont(new Font("微软雅黑", Font.PLAIN, 14));
-        testLogArea.setLineWrap(true);
-        JScrollPane testLogScroll = new JScrollPane(testLogArea);
-        testLogScroll.setBounds(80, 320, 700, 180);
-        indexPanelTest.add(testLogScroll);
-
-        tabbedPane.addTab("通讯测试", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), searchScrollPaneTest, null);
+        // 原「通讯测试」页已隐藏;改为 PLC 调试页,读取 DB200 全部点位
+        qmPlcMonitorPanel = new QmPlcMonitorPanel();
+        tabbedPane.addTab("PLC调试", new ImageIcon(MesClient.class.getResource("/bg/menu_data_preprocess.png")), qmPlcMonitorPanel, null);
 
         JPanel indexPanelTask = new JPanel();
         searchScrollPaneTask = new JScrollPane(indexPanelTask);
@@ -2102,16 +2054,34 @@ public class MesClient extends JFrame {
         tabbedPane.addChangeListener(new ChangeListener() {
             @Override
             public void stateChanged(ChangeEvent e) {
-                JTabbedPane tabbedPane = (JTabbedPane) e.getSource();
-                int selectedIndex = tabbedPane.getSelectedIndex();
-                if(selectedIndex == 1){
-
+                JTabbedPane pane = (JTabbedPane) e.getSource();
+                int selectedIndex = pane.getSelectedIndex();
+                String title = pane.getTitleAt(selectedIndex);
+                if ("PLC调试".equals(title)) {
+                    if (qmPlcMonitorPanel != null) {
+                        qmPlcMonitorPanel.startMonitor();
+                    }
+                } else if (qmPlcMonitorPanel != null) {
+                    qmPlcMonitorPanel.stopMonitor();
+                }
+                if (selectedIndex == 1) {
+                    JdbcUtils.getTestData();
                 }
-                JdbcUtils.getTestData();
             }
         });
     }
 
+    /** PLC 启用后:隐藏启动按钮、工件码只读 */
+    public static void applyPlcWorkUi() {
+        if (f_scan_data_bt_1 != null) {
+            f_scan_data_bt_1.setVisible(false);
+        }
+        if (product_sn != null) {
+            product_sn.setEditable(false);
+            product_sn.setBounds(81, 70, 810, 70);
+        }
+    }
+
     public static void setMenuStatus(String msg,int error){
         if(error == 0){
             MesClient.status_menu.setForeground(Color.GREEN);

+ 6 - 1
src/com/mes/util/SerialPortUtils.java

@@ -104,6 +104,9 @@ public class SerialPortUtils {
 //
                                                     }
                                                 } else if (data.length() == 97) {//检测结束
+                                                    if (MesClient.qmPlcEnabled) {
+                                                        return;
+                                                    }
                                                     log.info("检测结束");
                                                     //结果
                                                     String result = null;
@@ -236,7 +239,9 @@ public class SerialPortUtils {
                         }
                     });
 
-                    WorkTimer.start();
+                    if (!MesClient.qmPlcEnabled) {
+                        WorkTimer.start();
+                    }
                 }
             }
         }catch (Exception e) {

+ 11 - 0
src/com/mes/util/TestParam.java

@@ -17,6 +17,8 @@ public class TestParam {
     public String result;
     public String remark;
     public String deviceType;
+    /** 充气/填充时长(秒),对应上传 cq */
+    public String fillDuration;
     /** 稳压时长(秒),对应上传 by */
     public String stabilizeTime;
     /** 测试时长(秒),对应上传 cs */
@@ -134,6 +136,14 @@ public class TestParam {
         this.deviceType = deviceType;
     }
 
+    public String getFillDuration() {
+        return fillDuration;
+    }
+
+    public void setFillDuration(String fillDuration) {
+        this.fillDuration = fillDuration;
+    }
+
     public String getStabilizeTime() {
         return stabilizeTime;
     }
@@ -176,6 +186,7 @@ public class TestParam {
                 ", result='" + result + '\'' +
                 ", remark='" + remark + '\'' +
                 ", deviceType='" + deviceType + '\'' +
+                ", fillDuration='" + fillDuration + '\'' +
                 ", stabilizeTime='" + stabilizeTime + '\'' +
                 ", testDuration='" + testDuration + '\'' +
                 '}';

+ 3 - 0
src/com/mes/util/WorkTimer.java

@@ -70,6 +70,9 @@ public class WorkTimer {
                 // 如果开始工作,则循环查询测量结果
                 // 每项读法: 先 10 30 04,再读 压力/单位/泄漏/单位/状态
                 if (MesClient.work_status == 1) {
+                    if (MesClient.qmPlcEnabled) {
+                        return;
+                    }
                     final ATEQ.MeasureResult measureResult = ATEQ.readMeasureResult();
                     log.info(measureResult.toString());
 

+ 7 - 0
src/resources/config/config.properties

@@ -5,3 +5,10 @@ mes.tcp_port=3000
 mes.heart_beat_cycle=60
 mes.line_sn=XT
 portName1=COM9
+
+# 框架气密 PLC(DB200)
+mes.qm.plc.enabled=true
+mes.qm.plc.host=192.168.1.1
+mes.qm.plc.rack=0
+mes.qm.plc.slot=1
+mes.qm.plc.db=200