liuwei 2 týždňov pred
commit
2cec2b10f8
100 zmenil súbory, kde vykonal 4254 pridanie a 0 odobranie
  1. 24 0
      .classpath
  2. 11 0
      .gitignore
  3. 17 0
      .project
  4. 2 0
      image/.gitignore
  5. BIN
      image/background.png
  6. BIN
      image/bg/a_side.png
  7. BIN
      image/bg/b_side.png
  8. BIN
      image/bg/bar_add.png
  9. BIN
      image/bg/bar_bookmark.png
  10. BIN
      image/bg/bar_cmd_go.png
  11. BIN
      image/bg/bar_edit.png
  12. BIN
      image/bg/bar_pause.png
  13. BIN
      image/bg/bar_sampling.png
  14. BIN
      image/bg/bar_setting.png
  15. BIN
      image/bg/bar_stop.png
  16. BIN
      image/bg/close_bt.png
  17. BIN
      image/bg/company_setting_logo.png
  18. BIN
      image/bg/delete.png
  19. BIN
      image/bg/department_setting_logo.png
  20. BIN
      image/bg/download.png
  21. BIN
      image/bg/equipment_setting_logo.png
  22. BIN
      image/bg/gj_001_1_20.png
  23. BIN
      image/bg/gj_001_21_40.png
  24. BIN
      image/bg/gj_001_41_60.png
  25. BIN
      image/bg/green_dot.png
  26. BIN
      image/bg/grey_dot.png
  27. BIN
      image/bg/icon.png
  28. BIN
      image/bg/inductance_logo.png
  29. BIN
      image/bg/insulation_logo.png
  30. BIN
      image/bg/login.png
  31. BIN
      image/bg/logo.png
  32. BIN
      image/bg/logoff.png
  33. BIN
      image/bg/menu_data_analysis.png
  34. BIN
      image/bg/menu_data_preprocess.png
  35. BIN
      image/bg/menu_file.png
  36. BIN
      image/bg/menu_setting.png
  37. BIN
      image/bg/min_bt.png
  38. BIN
      image/bg/ng_bg.png
  39. BIN
      image/bg/ok_bg.png
  40. BIN
      image/bg/open_file.png
  41. BIN
      image/bg/refresh.png
  42. BIN
      image/bg/reset_logo.png
  43. BIN
      image/bg/resistance_logo.png
  44. BIN
      image/bg/save_bg.png
  45. BIN
      image/bg/scan_barcode.png
  46. BIN
      image/bg/tree_folder_icon.png
  47. BIN
      image/bg/tree_leaf_icon.png
  48. BIN
      image/bg/user.png
  49. BIN
      image/bg/vertical_line.png
  50. BIN
      image/bg/zoom_in.png
  51. BIN
      image/bg/zoom_out.png
  52. BIN
      image/close_bt.png
  53. 0 0
      image/cmd.txt
  54. BIN
      image/icon.ico
  55. BIN
      image/info_bt.png
  56. BIN
      image/min_bt.png
  57. BIN
      lib/bcpkix-jdk15on-1.69.jar
  58. BIN
      lib/bcprov-jdk15on-1.70.jar
  59. BIN
      lib/bcutil-jdk15on-1.69.jar
  60. BIN
      lib/commons-codec-1.15.jar
  61. BIN
      lib/core-3.5.1.jar
  62. BIN
      lib/failureaccess-1.0.1.jar
  63. BIN
      lib/fastjson2-2.0.16.jar
  64. BIN
      lib/gson-2.10.jar
  65. BIN
      lib/guava-31.0.1-jre.jar
  66. BIN
      lib/iot-communication-1.4.4.jar
  67. BIN
      lib/javase-3.5.1.jar
  68. BIN
      lib/jfreechart-1.5.4.jar
  69. BIN
      lib/jshortcut-0.4-oberzalek.jar
  70. BIN
      lib/kotlin-stdlib-1.8.10.jar
  71. BIN
      lib/logback-classic-1.2.13.jar
  72. BIN
      lib/logback-core-1.2.13.jar
  73. BIN
      lib/netty-all-4.1.48.Final.jar
  74. BIN
      lib/netty-channel-fsm-0.8.jar
  75. BIN
      lib/okhttp-4.10.0.jar
  76. BIN
      lib/okio-jvm-3.3.0.jar
  77. BIN
      lib/s7connector-2.1.jar
  78. BIN
      lib/sdk-client-0.6.9.jar
  79. BIN
      lib/sdk-core-0.6.9.jar
  80. BIN
      lib/slf4j-api-1.7.36.jar
  81. BIN
      lib/sqlite-jdbc-3.36.0.3.jar
  82. BIN
      lib/stack-client-0.6.9.jar
  83. BIN
      lib/stack-core-0.6.9.jar
  84. BIN
      lib/strict-machine-0.6.jar
  85. 3 0
      src/META-INF/MANIFEST.MF
  86. 13 0
      src/com/mes/component/CallFromJs.java
  87. 62 0
      src/com/mes/component/MesRadio.java
  88. 48 0
      src/com/mes/component/MesWebView.java
  89. 30 0
      src/com/mes/component/MyDialog.java
  90. 296 0
      src/com/mes/component/PlcSignalMonitor.java
  91. 1200 0
      src/com/mes/component/WorkJPanelOP40.java
  92. 469 0
      src/com/mes/component/WorkJPanelOP40Transfer.java
  93. 441 0
      src/com/mes/component/WorkJPanelScan.java
  94. 533 0
      src/com/mes/test/AlarmReadTest.java
  95. 495 0
      src/com/mes/test/ToolLifeTest.java
  96. 58 0
      src/com/mes/ui/BindMaterialResp.java
  97. 89 0
      src/com/mes/ui/CacheData.java
  98. 71 0
      src/com/mes/ui/CncParamReq.java
  99. 392 0
      src/com/mes/ui/DataUtil.java
  100. 0 0
      src/com/mes/ui/LoginFarme.java

+ 24 - 0
.classpath

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+		<attributes>
+			<attribute name="module" value="true"/>
+		</attributes>
+	</classpathentry>
+    <classpathentry kind="src" path="image"/>
+	<classpathentry excluding="resources/" kind="src" path="src"/>
+	<classpathentry kind="src" path="src/resources"/>
+	<classpathentry kind="output" path="bin"/>
+	<classpathentry kind="lib" path="lib/s7connector-2.1.jar"/>
+	<classpathentry kind="lib" path="lib/slf4j-api-2.0.6.jar"/>
+	<classpathentry kind="lib" path="lib/okhttp-4.10.0.jar"/>
+	<classpathentry kind="lib" path="lib/kotlin-stdlib-1.8.10.jar"/>
+	<classpathentry kind="lib" path="lib/okio-jvm-3.3.0.jar"/>
+	<classpathentry kind="lib" path="lib/gson-2.10.jar"/>
+	<classpathentry kind="lib" path="lib/jfreechart-1.5.4.jar"/>
+	<classpathentry kind="lib" path="lib/jshortcut-0.4-oberzalek.jar"/>
+	<classpathentry kind="lib" path="lib/netty-all-4.1.48.Final.jar"/>
+	<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"/>
+</classpath>

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+.idea
+.settings
+classes
+bin
+out
+*.db
+*.iml
+
+# 日志文件
+logs/
+*.log

+ 17 - 0
.project

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>mesclient</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>

+ 2 - 0
image/.gitignore

@@ -0,0 +1,2 @@
+META-INF
+_system~.ini

BIN
image/background.png


BIN
image/bg/a_side.png


BIN
image/bg/b_side.png


BIN
image/bg/bar_add.png


BIN
image/bg/bar_bookmark.png


BIN
image/bg/bar_cmd_go.png


BIN
image/bg/bar_edit.png


BIN
image/bg/bar_pause.png


BIN
image/bg/bar_sampling.png


BIN
image/bg/bar_setting.png


BIN
image/bg/bar_stop.png


BIN
image/bg/close_bt.png


BIN
image/bg/company_setting_logo.png


BIN
image/bg/delete.png


BIN
image/bg/department_setting_logo.png


BIN
image/bg/download.png


BIN
image/bg/equipment_setting_logo.png


BIN
image/bg/gj_001_1_20.png


BIN
image/bg/gj_001_21_40.png


BIN
image/bg/gj_001_41_60.png


BIN
image/bg/green_dot.png


BIN
image/bg/grey_dot.png


BIN
image/bg/icon.png


BIN
image/bg/inductance_logo.png


BIN
image/bg/insulation_logo.png


BIN
image/bg/login.png


BIN
image/bg/logo.png


BIN
image/bg/logoff.png


BIN
image/bg/menu_data_analysis.png


BIN
image/bg/menu_data_preprocess.png


BIN
image/bg/menu_file.png


BIN
image/bg/menu_setting.png


BIN
image/bg/min_bt.png


BIN
image/bg/ng_bg.png


BIN
image/bg/ok_bg.png


BIN
image/bg/open_file.png


BIN
image/bg/refresh.png


BIN
image/bg/reset_logo.png


BIN
image/bg/resistance_logo.png


BIN
image/bg/save_bg.png


BIN
image/bg/scan_barcode.png


BIN
image/bg/tree_folder_icon.png


BIN
image/bg/tree_leaf_icon.png


BIN
image/bg/user.png


BIN
image/bg/vertical_line.png


BIN
image/bg/zoom_in.png


BIN
image/bg/zoom_out.png


BIN
image/close_bt.png


+ 0 - 0
image/cmd.txt


BIN
image/icon.ico


BIN
image/info_bt.png


BIN
image/min_bt.png


BIN
lib/bcpkix-jdk15on-1.69.jar


BIN
lib/bcprov-jdk15on-1.70.jar


BIN
lib/bcutil-jdk15on-1.69.jar


BIN
lib/commons-codec-1.15.jar


BIN
lib/core-3.5.1.jar


BIN
lib/failureaccess-1.0.1.jar


BIN
lib/fastjson2-2.0.16.jar


BIN
lib/gson-2.10.jar


BIN
lib/guava-31.0.1-jre.jar


BIN
lib/iot-communication-1.4.4.jar


BIN
lib/javase-3.5.1.jar


BIN
lib/jfreechart-1.5.4.jar


BIN
lib/jshortcut-0.4-oberzalek.jar


BIN
lib/kotlin-stdlib-1.8.10.jar


BIN
lib/logback-classic-1.2.13.jar


BIN
lib/logback-core-1.2.13.jar


BIN
lib/netty-all-4.1.48.Final.jar


BIN
lib/netty-channel-fsm-0.8.jar


BIN
lib/okhttp-4.10.0.jar


BIN
lib/okio-jvm-3.3.0.jar


BIN
lib/s7connector-2.1.jar


BIN
lib/sdk-client-0.6.9.jar


BIN
lib/sdk-core-0.6.9.jar


BIN
lib/slf4j-api-1.7.36.jar


BIN
lib/sqlite-jdbc-3.36.0.3.jar


BIN
lib/stack-client-0.6.9.jar


BIN
lib/stack-core-0.6.9.jar


BIN
lib/strict-machine-0.6.jar


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

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

+ 13 - 0
src/com/mes/component/CallFromJs.java

@@ -0,0 +1,13 @@
+package com.mes.component;
+
+import com.mes.ui.MesClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CallFromJs {
+    public static final Logger log =  LoggerFactory.getLogger(CallFromJs.class);
+    public String callFromJs(String info){
+        log.info("ss:"+info);
+        return MesClient.sessionid+":java:" + info;
+    }
+}

+ 62 - 0
src/com/mes/component/MesRadio.java

@@ -0,0 +1,62 @@
+package com.mes.component;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class MesRadio extends JPanel {
+
+    public static final Logger log =  LoggerFactory.getLogger(MesRadio.class);
+
+    private String result = "";
+    private Integer i = 0;
+
+    public MesRadio(String[] radioTitles,String[] radioVals){
+        this.setLayout(new FlowLayout());
+        ButtonGroup group = new ButtonGroup();
+        i = 0;
+        for (String title:radioTitles){
+            JRadioButton radioButton = new JRadioButton(title);
+            radioButton.setFont(new Font("微软雅黑", Font.PLAIN, 28));
+
+            String val = radioVals[i];
+            radioButton.setName(val);
+
+            this.add(radioButton);
+            group.add(radioButton);
+
+            radioButton.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    result = val;
+                    log.info("选择:"+result);
+                }
+            });
+            i++;
+        }
+    }
+
+    public void setResult(String ret){
+        this.result = "";
+        Component[] components = this.getComponents();
+        for (Component jRadioButton:components){
+            if(jRadioButton instanceof JRadioButton){
+                JRadioButton jRadioButtonParam = (JRadioButton)jRadioButton;
+                if(ret.equals(jRadioButtonParam.getName())){
+                    jRadioButtonParam.setSelected(true);
+                    this.result = jRadioButtonParam.getName();
+                }else{
+                    jRadioButtonParam.setSelected(false);
+                }
+            }
+        }
+    }
+
+    public String getResult(){
+        return this.result;
+    }
+}

+ 48 - 0
src/com/mes/component/MesWebView.java

@@ -0,0 +1,48 @@
+package com.mes.component;
+
+import javafx.application.Platform;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Worker;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.scene.web.WebEngine;
+import javafx.scene.web.WebView;
+import netscape.javascript.JSObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+
+public class MesWebView extends JFXPanel {
+
+    public static final Logger log =  LoggerFactory.getLogger(MesWebView.class);
+
+    public static WebEngine engine = null;
+
+    public MesWebView(String url){
+
+        Platform.runLater(() -> {
+            WebView webView = new WebView();
+            setScene(new Scene(webView));
+            Date date = new Date();
+//            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesQuality/check?__sid="+sessionid+"&oprno="+mes_gw+"&t="+date.getTime();
+            log.info(url);
+            engine = webView.getEngine();
+            CallFromJs bridge = new CallFromJs();
+            engine.getLoadWorker().stateProperty().addListener(
+                    (ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) -> {
+                        if (newState == Worker.State.SUCCEEDED) {
+                            JSObject window = (JSObject) engine.executeScript("window");
+                            window.setMember("apps", bridge);
+                            window.setMember("sout", System.out);
+                        }
+                    }
+            );
+            engine.load(url);
+        });
+    }
+
+    public void reloadWeb(){
+        engine.reload();
+    }
+}

+ 30 - 0
src/com/mes/component/MyDialog.java

@@ -0,0 +1,30 @@
+package com.mes.component;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class MyDialog extends JDialog {
+
+    public MyDialog(Frame owner, String title, String msg){
+        super(owner, title);
+        init(msg);
+    }
+
+    private void init(String msg){
+        Container container = this.getContentPane();
+        container.setLayout(null);
+        container.setBackground(Color.BLACK);
+        this.setSize(800,220);
+        this.setLocationRelativeTo(null);
+
+        JLabel tslabel = new JLabel(msg);
+        tslabel.setHorizontalAlignment(SwingConstants.CENTER);
+        tslabel.setForeground(Color.RED);
+        tslabel.setFont(new Font("微软雅黑", Font.PLAIN,48));
+        tslabel.setBounds(0,30,800,100);
+
+        container.add(tslabel);
+        this.setModal(true);
+        this.setVisible(true);
+    }
+}

+ 296 - 0
src/com/mes/component/PlcSignalMonitor.java

@@ -0,0 +1,296 @@
+package com.mes.component;
+
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * PLC信号监控组件
+ * 可复用的信号监控面板,支持显示PLC信号状态和详细对话框
+ */
+public class PlcSignalMonitor {
+    
+    private static final Logger log = LoggerFactory.getLogger(PlcSignalMonitor.class);
+    
+    private S7PLC s7PLC;
+    private String plcDb;
+    private String stationName;
+    private JFrame parentFrame;
+    
+    // 信号标签(用于对话框中的实时更新)
+    private JLabel signal_feeding;
+    private JLabel signal_blanking;
+    private JLabel signal_running;
+    private JLabel signal_deviceStatus;
+    private JLabel signal_allowFeeding;
+    private JLabel signal_allowBlanking;
+    private JLabel signal_heart;
+    private JLabel signal_toDevice;
+    private JLabel signal_toTransfer;
+    private JLabel signal_transferToDevice;
+    
+    /**
+     * 构造函数
+     * @param s7PLC PLC连接对象
+     * @param plcDb DB块名称(如 DB7000)
+     * @param stationName 工位名称(如 OP040A)
+     * @param parentFrame 父窗口
+     */
+    public PlcSignalMonitor(S7PLC s7PLC, String plcDb, String stationName, JFrame parentFrame) {
+        this.s7PLC = s7PLC;
+        this.plcDb = plcDb;
+        this.stationName = stationName;
+        this.parentFrame = parentFrame;
+    }
+    
+    /**
+     * 创建信号监控按钮(放在工位面板中)
+     * @return 信号监控按钮
+     */
+    public JButton createSignalButton() {
+        JButton signalBtn = new JButton("PLC信号");
+        signalBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+        signalBtn.setForeground(new Color(0, 102, 204));
+        signalBtn.setToolTipText("点击查看PLC信号详情");
+        signalBtn.addActionListener(e -> showSignalDialog());
+        return signalBtn;
+    }
+    
+    /**
+     * 创建信号标签
+     */
+    private JLabel createSignalLabel(String text, boolean active) {
+        JLabel label = new JLabel(text);
+        label.setOpaque(true);
+        label.setFont(new Font("微软雅黑", Font.PLAIN, 11));
+        label.setHorizontalAlignment(SwingConstants.CENTER);
+        label.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+        label.setPreferredSize(new Dimension(50, 18));
+        updateSignalLabel(label, active);
+        return label;
+    }
+    
+    /**
+     * 更新信号标签状态
+     */
+    private void updateSignalLabel(JLabel label, boolean active) {
+        if (active) {
+            label.setBackground(new Color(0, 200, 0));
+            label.setForeground(Color.WHITE);
+        } else {
+            label.setBackground(new Color(200, 200, 200));
+            label.setForeground(Color.DARK_GRAY);
+        }
+    }
+    
+    /**
+     * 显示信号监控对话框
+     */
+    public void showSignalDialog() {
+        JDialog dialog = new JDialog(parentFrame, stationName + " - PLC信号监控", false);
+        dialog.setSize(550, 400);
+        dialog.setLocationRelativeTo(parentFrame);
+        
+        JPanel panel = new JPanel();
+        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
+        panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+        
+        // 标题
+        JLabel title = new JLabel("PLC信号实时监控");
+        title.setFont(new Font("微软雅黑", Font.BOLD, 16));
+        title.setAlignmentX(Component.CENTER_ALIGNMENT);
+        panel.add(title);
+        panel.add(Box.createVerticalStrut(10));
+        
+        // 工位信息
+        JLabel stationInfo = new JLabel("工位: " + stationName + " | DB块: " + plcDb);
+        stationInfo.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+        stationInfo.setForeground(Color.GRAY);
+        stationInfo.setAlignmentX(Component.CENTER_ALIGNMENT);
+        panel.add(stationInfo);
+        panel.add(Box.createVerticalStrut(15));
+        
+        // 信号详情面板
+        JPanel signalPanel = createSignalDetailPanel();
+        panel.add(signalPanel);
+        
+        panel.add(Box.createVerticalStrut(15));
+        
+        // 刷新按钮
+        JButton refreshBtn = new JButton("刷新信号");
+        refreshBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+        refreshBtn.setAlignmentX(Component.CENTER_ALIGNMENT);
+        refreshBtn.addActionListener(e -> updateSignalDisplay());
+        panel.add(refreshBtn);
+        
+        dialog.add(panel);
+        
+        // 首次显示时更新信号
+        updateSignalDisplay();
+        
+        dialog.setVisible(true);
+    }
+    
+    /**
+     * 创建信号详情面板
+     */
+    private JPanel createSignalDetailPanel() {
+        String[][] signals = {
+            {"请求扫码", plcDb + ".0.0", "PLC→MES"},
+            {"下料信号", plcDb + ".0.1", "PLC→MES"},
+            {"设备运行", plcDb + ".0.2", "PLC→MES"},
+            {"设备通讯", plcDb + ".0.3", "PLC状态"},
+            {"MES允许上料", plcDb + ".0.4", "MES→PLC"},
+            {"MES允许下料", plcDb + ".0.5", "MES→PLC"},
+            {"心跳信号", plcDb + ".0.6", "MES→PLC"},
+            {"上料到设备", plcDb + ".0.7", "PLC→MES"},
+            {"上料到中转车", plcDb + ".1.0", "PLC→MES"},
+            {"中转车上料到设备", plcDb + ".1.1", "PLC→MES"}
+        };
+        
+        JPanel panel = new JPanel(new GridLayout(signals.length + 1, 4, 5, 5));
+        panel.setBorder(BorderFactory.createTitledBorder(stationName + " 信号状态"));
+        
+        // 表头
+        panel.add(createHeaderLabel("信号名称"));
+        panel.add(createHeaderLabel("地址"));
+        panel.add(createHeaderLabel("方向"));
+        panel.add(createHeaderLabel("状态"));
+        
+        // 初始化信号标签
+        signal_feeding = createStatusLabel();
+        signal_blanking = createStatusLabel();
+        signal_running = createStatusLabel();
+        signal_deviceStatus = createStatusLabel();
+        signal_allowFeeding = createStatusLabel();
+        signal_allowBlanking = createStatusLabel();
+        signal_heart = createStatusLabel();
+        signal_toDevice = createStatusLabel();
+        signal_toTransfer = createStatusLabel();
+        signal_transferToDevice = createStatusLabel();
+        
+        JLabel[] statusLabels = {
+            signal_feeding, signal_blanking, signal_running, signal_deviceStatus,
+            signal_allowFeeding, signal_allowBlanking, signal_heart,
+            signal_toDevice, signal_toTransfer, signal_transferToDevice
+        };
+        
+        // 添加信号行
+        for (int i = 0; i < signals.length; i++) {
+            panel.add(new JLabel(signals[i][0], SwingConstants.CENTER));
+            panel.add(new JLabel(signals[i][1], SwingConstants.CENTER));
+            panel.add(new JLabel(signals[i][2], SwingConstants.CENTER));
+            panel.add(statusLabels[i]);
+        }
+        
+        return panel;
+    }
+    
+    /**
+     * 创建表头标签
+     */
+    private JLabel createHeaderLabel(String text) {
+        JLabel label = new JLabel(text, SwingConstants.CENTER);
+        label.setFont(new Font("微软雅黑", Font.BOLD, 12));
+        return label;
+    }
+    
+    /**
+     * 创建状态标签
+     */
+    private JLabel createStatusLabel() {
+        JLabel label = new JLabel("--", SwingConstants.CENTER);
+        label.setOpaque(true);
+        label.setBackground(Color.LIGHT_GRAY);
+        label.setFont(new Font("微软雅黑", Font.PLAIN, 11));
+        return label;
+    }
+    
+    /**
+     * 更新信号显示
+     */
+    private void updateSignalDisplay() {
+        try {
+            if (s7PLC == null) {
+                log.warn("[{}] PLC连接为空,无法读取信号", stationName);
+                showConnectionError();
+                return;
+            }
+            
+            // 读取所有信号
+            boolean feeding = s7PLC.readBoolean(plcDb + ".0.0");
+            boolean blanking = s7PLC.readBoolean(plcDb + ".0.1");
+            boolean running = s7PLC.readBoolean(plcDb + ".0.2");
+            boolean deviceStatus = s7PLC.readBoolean(plcDb + ".0.3");
+            boolean allowFeeding = s7PLC.readBoolean(plcDb + ".0.4");
+            boolean allowBlanking = s7PLC.readBoolean(plcDb + ".0.5");
+            boolean heart = s7PLC.readBoolean(plcDb + ".0.6");
+            boolean toDevice = s7PLC.readBoolean(plcDb + ".0.7");
+            boolean toTransfer = s7PLC.readBoolean(plcDb + ".1.0");
+            boolean transferToDevice = s7PLC.readBoolean(plcDb + ".1.1");
+            
+            // 在EDT线程中更新UI
+            SwingUtilities.invokeLater(() -> {
+                updateStatusLabel(signal_feeding, feeding);
+                updateStatusLabel(signal_blanking, blanking);
+                updateStatusLabel(signal_running, running);
+                updateStatusLabel(signal_deviceStatus, deviceStatus);
+                updateStatusLabel(signal_allowFeeding, allowFeeding);
+                updateStatusLabel(signal_allowBlanking, allowBlanking);
+                updateStatusLabel(signal_heart, heart);
+                updateStatusLabel(signal_toDevice, toDevice);
+                updateStatusLabel(signal_toTransfer, toTransfer);
+                updateStatusLabel(signal_transferToDevice, transferToDevice);
+            });
+            
+        } catch (Exception e) {
+            String errorMsg = e.getMessage();
+            if (errorMsg != null && errorMsg.contains("connect timed out")) {
+                log.warn("[{}] PLC连接超时,请检查网络连接", stationName);
+            } else {
+                log.error("[{}] 读取PLC信号异常: {}", stationName, errorMsg);
+            }
+            // 在UI上显示错误提示
+            SwingUtilities.invokeLater(this::showConnectionError);
+        }
+    }
+    
+    /**
+     * 显示连接错误提示
+     */
+    private void showConnectionError() {
+        JLabel[] labels = {
+            signal_feeding, signal_blanking, signal_running, signal_deviceStatus,
+            signal_allowFeeding, signal_allowBlanking, signal_heart,
+            signal_toDevice, signal_toTransfer, signal_transferToDevice
+        };
+        
+        for (JLabel label : labels) {
+            if (label != null) {
+                label.setText("ERR");
+                label.setBackground(Color.ORANGE);
+                label.setForeground(Color.WHITE);
+            }
+        }
+    }
+    
+    /**
+     * 更新状态标签
+     */
+    private void updateStatusLabel(JLabel label, boolean active) {
+        if (label != null) {
+            if (active) {
+                label.setText("ON");
+                label.setBackground(new Color(0, 200, 0));
+                label.setForeground(Color.WHITE);
+            } else {
+                label.setText("OFF");
+                label.setBackground(new Color(200, 200, 200));
+                label.setForeground(Color.DARK_GRAY);
+            }
+        }
+    }
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1200 - 0
src/com/mes/component/WorkJPanelOP40.java


+ 469 - 0
src/com/mes/component/WorkJPanelOP40Transfer.java

@@ -0,0 +1,469 @@
+package com.mes.component;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import com.mes.ui.DataUtil;
+import com.mes.ui.MesClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class WorkJPanelOP40Transfer extends JPanel {
+
+    public static final Logger log = LoggerFactory.getLogger(WorkJPanelOP40Transfer.class);
+
+    public String oprno = "OP040";
+
+    public String pos = "A";
+
+    public String lastSn = ""; // 最近的码
+
+    public String lineSn = "XT";
+
+    public JLabel feedinglabel;           // 请求扫码信号
+
+    public JLabel feedingmeslabel;        // MES扫码反馈
+
+    public JLabel feedingtotransferlabel; // 去中转台信号
+
+    public JTextField productSnFeeding;   // 工件码显示
+
+    public JButton status_menu;           // 状态显示
+
+    public JButton resetFeedingBtn;       // 刷新按钮
+    
+    public JButton signalBtn;             // 信号监控按钮
+    
+    public PlcSignalMonitor signalMonitor; // 信号监控组件
+
+    public ImageIcon imageGreen;
+
+    public ImageIcon imageGray;
+
+    public String plcIp1 = "192.168.90.83"; // PLC3 西侧
+    public String plcIp2 = "192.168.90.84"; // PLC4 东侧
+
+    // 只连接一个PLC
+    public S7PLC s7PLC = null;
+
+    public Integer workWidth = 470;
+    public Integer workHeight = 300;
+
+    public String plcDb = "DB7005"; // 共用工位6的DB块
+
+    public Integer tjFeedingFlag = 0; // 上料状态标识: 0=等待请求, 1=已获取工件码等待清空
+    public Boolean toTransfer = false; // 去中转台信号(用于触发清空)
+    
+    public int scanRetryCount = 0; // 扫码重试计数器,最多重试1次(共2次扫码机会)
+    public static final int MAX_SCAN_RETRY = 1; // 最大重试次数
+
+    public  Boolean plcHeart = false;
+    
+    // 用于检测状态变化的上一次值(减少日志输出)
+    private java.util.Map<String, Boolean> lastReadValues = new java.util.HashMap<>();
+    private java.util.Map<String, Boolean> lastWriteValues = new java.util.HashMap<>();
+
+    public Integer isLocal = 0; // 0=正式 1=本地
+
+    public WorkJPanelOP40Transfer(String position) {
+        this.setLayout(null);
+        this.setBackground(Color.WHITE);
+        this.pos = position;
+
+        // 根据位置选择连接的PLC,DB块都是DB7005(共用工位6的DB块)
+        if (pos.equals("A")) {
+            // 中转车1 连接 PLC3(西侧)
+            s7PLC = new S7PLC(EPlcType.S1500, plcIp1);
+        } else if (pos.equals("B")) {
+            // 中转车2 连接 PLC4(东侧)
+            s7PLC = new S7PLC(EPlcType.S1500, plcIp2);
+        }
+
+        JLabel oprnoLabel = new JLabel("中转车" + (pos.equals("A") ? "1" : "2"));
+        oprnoLabel.setHorizontalAlignment(SwingConstants.CENTER);
+        oprnoLabel.setForeground(Color.BLACK);
+        oprnoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        oprnoLabel.setBounds(0, 0, workWidth / 5, 40);
+        this.add(oprnoLabel);
+
+        status_menu = new JButton("等待请求");
+        status_menu.setBounds(workWidth / 5, 0, workWidth * 4 / 5, 40);
+        status_menu.setForeground(Color.GREEN);
+        status_menu.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        status_menu.setBackground(Color.BLACK);
+        this.add(status_menu);
+
+        JLabel oprnoLabelFeeding = new JLabel("上料");
+        oprnoLabelFeeding.setHorizontalAlignment(SwingConstants.CENTER);
+        oprnoLabelFeeding.setForeground(Color.BLACK);
+        oprnoLabelFeeding.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        oprnoLabelFeeding.setBounds(0, 50, workWidth / 5, 40);
+        this.add(oprnoLabelFeeding);
+
+        productSnFeeding = new JTextField("");
+        productSnFeeding.setHorizontalAlignment(SwingConstants.CENTER);
+        productSnFeeding.setForeground(Color.BLACK);
+        productSnFeeding.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        productSnFeeding.setEditable(false);
+        productSnFeeding.setBounds(workWidth / 5, 50, workWidth * 3 / 5, 40);
+        productSnFeeding.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                String sn = productSnFeeding.getText().trim();
+                if (!sn.isEmpty()) {
+                    MesClient.createQRCode(productSnFeeding.getText(), oprno + pos);
+                }
+            }
+        });
+        this.add(productSnFeeding);
+
+        resetFeedingBtn = new JButton("刷新");
+        resetFeedingBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                int choice = JOptionPane.showConfirmDialog(null, "确定刷新工件吗?", "操作确认", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                if (choice == JOptionPane.YES_OPTION) {
+                    resetFeeding();
+                }
+            }
+        });
+        resetFeedingBtn.setFont(new Font("微软雅黑", Font.PLAIN, 22));
+        resetFeedingBtn.setBounds(workWidth * 4 / 5 + 10, 50, workWidth / 5 - 10, 40);
+        this.add(resetFeedingBtn);
+
+        imageGreen = new ImageIcon(MesClient.class.getResource("/bg/green_dot.png"));
+        imageGreen.setImage(imageGreen.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH));
+
+        imageGray = new ImageIcon(MesClient.class.getResource("/bg/grey_dot.png"));
+        imageGray.setImage(imageGray.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH));
+
+        feedinglabel = new JLabel("请求扫码");
+        feedinglabel.setHorizontalAlignment(SwingConstants.LEFT);
+        feedinglabel.setIcon(imageGreen);
+        feedinglabel.setForeground(Color.BLACK);
+        feedinglabel.setBounds(0, 150, workWidth / 3, 30);
+        this.add(feedinglabel);
+
+        feedingmeslabel = new JLabel("MES扫码反馈");
+        feedingmeslabel.setHorizontalAlignment(SwingConstants.LEFT);
+        feedingmeslabel.setIcon(imageGreen);
+        feedingmeslabel.setForeground(Color.BLACK);
+        feedingmeslabel.setBounds(workWidth / 3, 150, workWidth / 3, 30);
+        feedingmeslabel.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                boolean ret = readAllowFeeding();
+                int choice = JOptionPane.showConfirmDialog(null, "确定修改MES扫码反馈结果为" + (ret ? "false" : "true") + "吗?", "操作确认", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                if (choice == JOptionPane.YES_OPTION) {
+                    writeAllowFeeding(!ret);
+                }
+            }
+        });
+        this.add(feedingmeslabel);
+
+        feedingtotransferlabel = new JLabel("去中转台");
+        feedingtotransferlabel.setHorizontalAlignment(SwingConstants.LEFT);
+        feedingtotransferlabel.setIcon(imageGray);
+        feedingtotransferlabel.setForeground(Color.BLACK);
+        feedingtotransferlabel.setBounds(workWidth * 2 / 3, 150, workWidth / 3, 30);
+        this.add(feedingtotransferlabel);
+        
+        // 信号监控按钮
+        String stationName = "中转车" + (pos.equals("A") ? "1(西侧)" : "2(东侧)");
+        signalMonitor = new PlcSignalMonitor(s7PLC, plcDb, stationName, MesClient.mesClientFrame);
+        signalBtn = signalMonitor.createSignalButton();
+        signalBtn.setBounds(4, 190, 90, 26);
+        this.add(signalBtn);
+
+        startOprnoTimer();
+        heartData();
+
+        resetScanA();
+    }
+
+    public void setMenuStatus(String msg, int error) {
+        if (error == 0) {
+            this.status_menu.setForeground(Color.GREEN);
+        } else {
+            this.status_menu.setForeground(Color.RED);
+        }
+        this.status_menu.setText(msg);
+    }
+
+    // 定时任务 -----------------------------------------------------------------------------------------------------
+
+    public Timer cjTimerText2;
+
+    public void heartData() {
+        // 中转车不写心跳,由工位F/L (DB7005) 统一写入
+        // 避免同一个地址被多个组件写入导致冲突
+        // 
+        // 原代码(已禁用):
+        // if (cjTimerText2 != null) {
+        //     cjTimerText2.cancel();
+        // }
+        // cjTimerText2 = new Timer();
+        // cjTimerText2.schedule(new TimerTask() {
+        //     public void run() {
+        //         if (MesClient.isLogin == 1 && isLocal == 0) {
+        //             plcHeart = !plcHeart;
+        //             writeBoolean(plcDb + ".0.6", plcHeart);
+        //         }
+        //     }
+        // }, 1000, 1000);
+    }
+
+    public Timer oprnoTimer;
+
+    public void startOprnoTimer() {
+
+        //心跳显示图标
+        if (oprnoTimer != null) {
+            oprnoTimer.cancel();
+        }
+        oprnoTimer = new Timer();
+        oprnoTimer.schedule(new TimerTask() {
+            public void run() {
+                try {
+
+                    if (MesClient.isLogin == 1) {
+                        // 读取PLC信号并更新UI
+                        Boolean retFeeding = readFeeding();
+                        feedinglabel.setIcon(retFeeding ? imageGreen : imageGray);
+
+                        Boolean retFeedingAllow = readAllowFeeding();
+                        feedingmeslabel.setIcon(retFeedingAllow ? imageGreen : imageGray);
+
+                        toTransfer = readFeedingToTransfer();
+                        feedingtotransferlabel.setIcon(toTransfer ? imageGreen : imageGray);
+
+                        // 请求信号消失时,清除MES反馈
+                        if (!retFeeding && retFeedingAllow) {
+                            writeAllowFeeding(false);
+                        }
+
+                        // 状态机逻辑(简化版)
+                        // 扫码和saveSn由工位6(F/L)的barCodeForTransfer()方法处理
+                        // 中转车面板只负责显示和重置
+                        if (tjFeedingFlag == 1) {
+                            // 状态1:已获取工件码,等待toTransfer信号变为false后清空
+                            if (!toTransfer) {
+                                log.info("[" + oprno + pos + "]toTransfer变为false,清空工件码");
+                                resetFeeding();
+                            }
+                        }
+                    }
+
+                } catch (Exception e) {
+                    log.error(e.getMessage());
+                }
+            }
+        }, 100, 3000);
+    }
+
+    public void barCode() {
+        if (!productSnFeeding.getText().isEmpty()) {
+            return;
+        }
+        String sn = readQrcode();
+        if (sn.equals("")) {
+            if (productSnFeeding.getText().isEmpty()) {
+                productSnFeeding.setText("空结果");
+                this.setMenuStatus("获取工件码为空", -1);
+            }
+        } else {
+            productSnFeeding.setText(sn);
+            lastSn = sn;
+            // 查询工件质量
+            JSONObject retObj = DataUtil.checkQuality(sn, oprno + pos, MesClient.user_menu.getText());
+            if (retObj.get("result") != null && retObj.get("result").toString().equalsIgnoreCase("true")) {
+                tjFeedingFlag = 1;//可加工
+                setMenuStatus("工件可以加工", 0);
+                writeAllowFeeding(true);
+                resetFeedingBtn.setEnabled(false);
+            } else {
+                writeAllowFeeding(false);
+                resetFeedingBtn.setEnabled(true);
+                if (retObj.get("result") == null) {
+                    setMenuStatus("请求失败,请重试", -1);
+                } else {
+                    if (retObj.get("result").toString().equalsIgnoreCase("false")) {
+                        setMenuStatus(retObj.getString("message"), -1);
+                    }
+                }
+            }
+        }
+    }
+
+    public void resetFeeding() {
+        productSnFeeding.setText("");
+        tjFeedingFlag = 0;
+        scanRetryCount = 0; // 重置重试计数器
+        setMenuStatus("等待请求", 0);
+        resetFeedingBtn.setEnabled(true);
+        writeAllowFeeding(false);
+    }
+
+    public void resetScanA() {
+        tjFeedingFlag = 0;
+        scanRetryCount = 0; // 重置重试计数器
+        productSnFeeding.setText("");
+        setMenuStatus("等待请求", 0);
+        resetFeedingBtn.setEnabled(true);
+        // 延迟写入PLC,避免初始化时连接超时
+        if(MesClient.isLogin == 1) {
+            writeAllowFeeding(false);
+        }
+    }
+
+    // 与设备交互-----------------------------------------------------------------------------------------
+
+    // 请求扫码信号 (DB7005.0.0)
+    public boolean readFeeding() {
+        if (isLocal == 1) {
+            return false;
+        } else {
+            return readBoolean(plcDb + ".0.0");
+        }
+    }
+
+    // 读取去中转台信号 (DB7005.1.0)
+    public boolean readFeedingToTransfer() {
+        if (isLocal == 1) {
+            return false;
+        } else {
+            return readBoolean(plcDb + ".1.0");
+        }
+    }
+
+    // 读取MES允许上料信号 (DB7005.0.4)
+    public boolean readAllowFeeding() {
+        if (isLocal == 1) {
+            return false;
+        } else {
+            return readBoolean(plcDb + ".0.4");
+        }
+    }
+
+    // 写入MES允许上料信号 (DB7005.0.4)
+    public boolean writeAllowFeeding(Boolean b) {
+        if (isLocal == 1) {
+            return true;
+        } else {
+            return writeBoolean(plcDb + ".0.4", b);
+        }
+    }
+
+    // 读取中转车号 (DB7005.2.0)
+    public int readTransferNo() {
+        if (isLocal == 1) {
+            return 1;
+        } else {
+            return s7PLC.readInt16(plcDb + ".2.0");
+        }
+    }
+
+    // 读取中转车层数 (DB7005.4.0)
+    public int readTransferFloor() {
+        if (isLocal == 1) {
+            return 1;
+        } else {
+            return s7PLC.readInt16(plcDb + ".4.0");
+        }
+    }
+
+    // 中转车不扫码,直接从数据库获取工件码
+    public String readQrcode() {
+        String sn = "";
+        if (isLocal == 1) {
+            return "";
+        } else {
+            try {
+
+//                if(this.pos.equals("A")){
+//                    sn = MesClient.scanQrcode1();
+//                }else{
+//                    sn = MesClient.scanQrcode2();
+//                }
+
+                if(pos.equals("A")){
+                    log.info("[{}]触发西侧摄像头扫码", oprno+pos);
+//                        sn = MesClient.scanQrcode1();
+                    sn = MesClient.scanSnA;
+                }else{
+                    log.info("[{}]触发东侧摄像头扫码", oprno+pos);
+                    sn = MesClient.scanSnB;
+//                        sn = MesClient.scanQrcode2();
+                }
+            } catch (Exception e) {
+                log.error("[" + oprno + pos + "]获取工件码异常: " + e.getMessage(), e);
+            }
+        }
+
+        return sn;
+    }
+
+    // 读取布尔值(只在值变化时打印日志)
+    public boolean readBoolean(String db) {
+        boolean permit;
+        try {
+            // 检查PLC连接是否可用
+            if(s7PLC == null) {
+                return false;
+            }
+            permit = s7PLC.readBoolean(db);
+            // 只在值变化时打印日志
+            Boolean lastValue = lastReadValues.get(db);
+            if (lastValue == null || lastValue != permit) {
+                log.info("<" + this.oprno + this.pos + ">读取" + db + "-->成功,结果:{}", permit ? "true" : "false");
+                lastReadValues.put(db, permit);
+            }
+        } catch (Exception e) {
+            // 连接超时时不打印完整堆栈
+            if(e.getMessage() != null && e.getMessage().contains("connect timed out")) {
+                // 连接超时是常见情况,降低日志级别
+            } else {
+                log.error("<" + this.oprno + this.pos + ">读取" + db + "-->失败: {}", e.getMessage());
+            }
+            return false;
+        }
+        return permit;
+    }
+
+    // 写入布尔值(只在值变化时打印日志)
+    public boolean writeBoolean(String db, boolean permit) {
+        try {
+            // 检查PLC连接是否可用
+            if(s7PLC == null) {
+                log.warn("<" + this.oprno + this.pos + ">PLC未初始化,跳过写入" + db);
+                return false;
+            }
+            // 只在值变化时打印日志
+            Boolean lastValue = lastWriteValues.get(db);
+            if (lastValue == null || lastValue != permit) {
+                s7PLC.writeBoolean(db, permit);
+                log.info("<" + this.oprno + this.pos + ">写入" + db + "-->成功,写入内容:{}", permit ? "true" : "false");
+                lastWriteValues.put(db, permit);
+            } else {
+                // 值未变化,仍然执行写入但不打印日志
+                s7PLC.writeBoolean(db, permit);
+            }
+        } catch (Exception e) {
+            // 连接超时时不打印完整堆栈
+            if(e.getMessage() != null && e.getMessage().contains("connect timed out")) {
+                log.warn("<" + this.oprno + this.pos + ">写入" + db + "-->连接超时(PLC未连接)");
+            } else {
+                log.error("<" + this.oprno + this.pos + ">写入" + db + "-->失败,写入内容:{}", permit ? "true" : "false");
+                log.error(e.getMessage());
+            }
+            return false;
+        }
+        return true;
+    }
+}

+ 441 - 0
src/com/mes/component/WorkJPanelScan.java

@@ -0,0 +1,441 @@
+package com.mes.component;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import com.mes.ui.CacheData;
+import com.mes.ui.DataUtil;
+import com.mes.ui.MesClient;
+import com.mes.util.JdbcUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Timer;
+import java.util.TimerTask;
+
+public class WorkJPanelScan extends JPanel {
+
+    public static final Logger log =  LoggerFactory.getLogger(WorkJPanelScan.class);
+
+    public String oprno = "OP040";
+
+    public String pos = "A";
+
+    public String lastSn = ""; // 最近的码
+
+    public String lineSn = "XT";
+
+    public JLabel feedinglabel;
+
+    public JLabel feedingmeslabel;
+
+    public JLabel runlabel; //运行反馈,
+
+    public JLabel blankinglabel; // 下料反馈
+
+    public JLabel blankingmeslabel; // 下料MES反馈
+
+    public JLabel feedingtodevicelabel; // 上料到设备
+
+    public JLabel feedingtotransferlabel; // 上料到中转车
+
+    public JLabel transfertodevicelabel; // 上料到中转车
+
+    public JButton scanBtn;
+
+    public JTextField productSn;
+
+    public JTextField productSnFeeding;
+
+    public JButton status_menu;
+
+    public JButton resetBtn;
+
+    public JButton resetFeedingBtn;
+
+    public ImageIcon imageGreen;
+
+    public ImageIcon imageGray;
+
+    public String plcIp1 = "192.168.90.83"; // plc3
+    public String plcIp2 = "192.168.90.84"; // plc4
+
+    public S7PLC s7PLC = null;
+
+    public Integer workWidth = 470;
+    public Integer workHeight = 300;
+
+    public String plcDb = "DB35";
+
+    public Integer tjFeedingFlag = 0; // 上料状态标识
+    public Integer tjFlag = 0; // 1=可加工 2=设备运行中 3=加工结束
+    public Integer tjStatus = 0; // 提交标识
+    public Boolean toDevice = false;
+    public Boolean toTransfer = false;
+    public Boolean transferToDevice = false;
+
+    public Boolean plcHeart = false;
+
+    public Integer isLocal = 0; // 0=正式 1=本地
+
+    public Integer isScan = 1; // 1=西侧 2=东测
+
+    public int transferSn = 0;
+    public int transferFloor = 0;
+
+    public CacheData cacheData = null;
+    public String cacheName = "";
+
+    public WorkJPanelScan(String position){
+        this.setLayout(null);
+        this.setBackground(Color.WHITE);
+        this.pos = position;
+        if(position.equals("A")){
+            s7PLC = new S7PLC(EPlcType.S1500, plcIp1);
+        }else{
+            s7PLC = new S7PLC(EPlcType.S1500, plcIp2);
+        }
+
+        cacheName = "cache_scan_"+oprno+pos;
+        cacheData = JdbcUtils.getCache(cacheName);
+        if(cacheData == null){
+            cacheData = new CacheData();
+            cacheData.setLastSn("");
+            cacheData.setSn("");
+            cacheData.setFeedingSn("");
+            cacheData.setTjStatus(0);
+            cacheData.setTjFlag(0);
+            cacheData.setError(-1);
+            cacheData.setMsg("");
+            JdbcUtils.setCache(cacheName, JSON.toJSONString(cacheData));
+        }
+
+        JLabel oprnoLabel = new JLabel(oprno);
+        oprnoLabel.setHorizontalAlignment(SwingConstants.CENTER);
+        oprnoLabel.setForeground(Color.BLACK);
+        oprnoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        oprnoLabel.setBounds(0, 0, workWidth/5, 40);
+        this.add(oprnoLabel);
+        if(this.pos.equals("A")){
+            oprnoLabel.setText("西侧");
+        }else{
+            oprnoLabel.setText("东侧");
+        }
+
+        status_menu = new JButton("等待请求扫码");
+        status_menu.setBounds(workWidth/5, 0, workWidth*4/5, 40);
+        status_menu.setForeground(Color.GREEN);
+        status_menu.setForeground(Color.GREEN);
+        status_menu.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        status_menu.setBackground(Color.BLACK);
+        this.add(status_menu);
+
+        JLabel oprnoLabelFeeding = new JLabel("扫码");
+        oprnoLabelFeeding.setHorizontalAlignment(SwingConstants.CENTER);
+        oprnoLabelFeeding.setForeground(Color.BLACK);
+        oprnoLabelFeeding.setFont(new Font("微软雅黑", Font.PLAIN, 20));
+        oprnoLabelFeeding.setBounds(0, 50, workWidth/5, 40);
+        this.add(oprnoLabelFeeding);
+
+        productSnFeeding = new JTextField("");
+        productSnFeeding.setHorizontalAlignment(SwingConstants.CENTER);
+        productSnFeeding.setForeground(Color.BLACK);
+        productSnFeeding.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        productSnFeeding.setEditable(false);
+        productSnFeeding.setBounds(workWidth/5, 50, workWidth*3/5, 40);
+        productSnFeeding.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                String sn = productSnFeeding.getText().trim();
+                if(!sn.isEmpty()){
+                    MesClient.createQRCode(productSnFeeding.getText(),oprno+pos);
+                }
+            }
+        });
+        this.add(productSnFeeding);
+
+        resetFeedingBtn = new JButton("刷新");
+        resetFeedingBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                int choice = JOptionPane.showConfirmDialog(null, "确定刷新上料工件吗?", "操作确认", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                if (choice == JOptionPane.YES_OPTION) {
+                    resetScanA();
+                }
+            }
+        });
+        resetFeedingBtn.setFont(new Font("微软雅黑", Font.PLAIN, 22));
+        resetFeedingBtn.setBounds(workWidth*4/5 + 10, 50, workWidth/5 - 10, 40);
+        this.add(resetFeedingBtn);
+
+        imageGreen = new ImageIcon(MesClient.class.getResource("/bg/green_dot.png"));
+        imageGreen.setImage(imageGreen.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH));
+
+        imageGray = new ImageIcon(MesClient.class.getResource("/bg/grey_dot.png"));
+        imageGray.setImage(imageGray.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH));
+
+        feedinglabel = new JLabel("请求扫码");
+        feedinglabel.setHorizontalAlignment(SwingConstants.LEFT);
+        feedinglabel.setIcon(imageGreen);
+        feedinglabel.setForeground(Color.BLACK);
+        feedinglabel.setBounds(0, 150, workWidth/3, 30);
+        this.add(feedinglabel);
+
+        feedingmeslabel = new JLabel("MES扫码反馈");
+        feedingmeslabel.setHorizontalAlignment(SwingConstants.LEFT);
+        feedingmeslabel.setIcon(imageGreen);
+        feedingmeslabel.setForeground(Color.BLACK);
+        feedingmeslabel.setBounds(workWidth/3, 150, workWidth/3, 30);
+        feedingmeslabel.addMouseListener(new MouseAdapter() {
+            public void mouseClicked(MouseEvent e) {
+                boolean ret = readScanFinish();
+                int choice = JOptionPane.showConfirmDialog(null, "确定修改MES扫码反馈结果为"+(ret?"false":"true")+"吗?", "操作确认", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                if (choice == JOptionPane.YES_OPTION) {
+                    writeScanFinish(!ret);
+                }
+            }
+        });
+        this.add(feedingmeslabel);
+
+//        transfertodevicelabel = new JLabel("中转车上料到设备");
+//        transfertodevicelabel.setHorizontalAlignment(SwingConstants.LEFT);
+//        transfertodevicelabel.setIcon(imageGreen);
+//        transfertodevicelabel.setForeground(Color.BLACK);
+//        transfertodevicelabel.setBounds(workWidth*2/3, 150, workWidth/3, 30);
+//        this.add(transfertodevicelabel);
+//
+//        blankinglabel = new JLabel("中转车号");
+//        blankinglabel.setHorizontalAlignment(SwingConstants.LEFT);
+//        blankinglabel.setIcon(imageGreen);
+//        blankinglabel.setForeground(Color.BLACK);
+//        blankinglabel.setBounds(0, 190, workWidth/3, 30);
+//        this.add(blankinglabel);
+//
+//        blankingmeslabel = new JLabel("放置层数");
+//        blankingmeslabel.setHorizontalAlignment(SwingConstants.LEFT);
+//        blankingmeslabel.setIcon(imageGreen);
+//        blankingmeslabel.setForeground(Color.BLACK);
+//        blankingmeslabel.setBounds(workWidth/3, 190, workWidth/3, 30);
+//        this.add(blankingmeslabel);
+
+        startOprnoTimer();
+        resetScanA();
+
+        formatElementUI();
+
+    }
+
+    public void formatElementUI(){
+        cacheData = JdbcUtils.getCache(cacheName);
+        if(cacheData == null){
+            return;
+        }
+        String msg = cacheData.getMsg();
+        int error = cacheData.getError();
+//        String sn = cacheData.getSn();
+//        productSn.setText(sn);
+
+        String feedingSn = cacheData.getFeedingSn();
+        productSnFeeding.setText(feedingSn);
+
+        tjFlag = cacheData.getTjFlag();
+        lastSn = cacheData.getLastSn();
+
+        setMenuStatus(msg,error);
+
+        this.repaint();
+    }
+
+    public void setMenuStatus(String msg,int error){
+        if(error == 0){
+            this.status_menu.setForeground(Color.GREEN);
+        }else{
+            this.status_menu.setForeground(Color.RED);
+        }
+        this.status_menu.setText(msg);
+
+        cacheData.setMsg(msg);
+        cacheData.setError(error);
+        cacheData.setTjFlag(tjFlag);
+//        cacheData.setTjStatus(tjStatus);
+        cacheData.setLastSn(lastSn);
+        cacheData.setSn(productSnFeeding.getText());
+        cacheData.setFeedingSn(productSnFeeding.getText());
+        JdbcUtils.setCache(cacheName, JSON.toJSONString(cacheData));
+    }
+
+    // 定时任务 -----------------------------------------------------------------------------------------------------
+
+    public Timer oprnoTimer;
+    public void startOprnoTimer() {
+
+        //心跳显示图标
+        if(oprnoTimer!=null) {
+            oprnoTimer.cancel();
+        }
+        oprnoTimer = new Timer();
+        oprnoTimer.schedule(new TimerTask() {
+            public void run() {
+                try{
+                    if(MesClient.isLogin == 1){
+                        Boolean retFeeding = readScanFeeding();
+                        feedinglabel.setIcon(retFeeding?imageGreen:imageGray);
+
+                        Boolean retFeedingAllow = readScanFinish();
+                        feedingmeslabel.setIcon(retFeedingAllow?imageGreen:imageGray);
+
+                        if(retFeeding){
+                            barCode2();
+                        }else{
+                            writeScanFinish(false);
+                        }
+                    }
+                }catch (Exception e) {
+                    log.error(e.getMessage());
+                }
+            }
+        }, 100,1500);
+    }
+
+    public void barCode2(){
+        String sn = readQrcode();
+        if(pos.equals("A")){
+            MesClient.scanSnA = sn.trim();
+        }else{
+            MesClient.scanSnB = sn.trim();
+        }
+        if(sn.length() != 19){
+            setMenuStatus("扫码结果为空", -1);
+        }else{
+            setMenuStatus("扫码成功", 0);
+            productSnFeeding.setText(sn);
+            writeScanFinish(true);
+        }
+    }
+
+    public void resetFeeding() {
+        productSnFeeding.setText("");
+        tjFeedingFlag = 0;
+        setMenuStatus("等待请求扫码",0);
+        resetFeedingBtn.setEnabled(true);
+//        writeAllowFeeding(false);
+    }
+
+    public void resetScanA() {
+        tjFeedingFlag = 0;
+        tjFlag = 0;
+        tjStatus = 0;
+//        productSnFeeding.setText("5001040043561100050");
+        productSnFeeding.setText("");
+//        setMenuStatus("等待请求扫码",0);
+
+        if(isLocal == 0){
+            writeScanFinish(false);
+        }
+    }
+
+
+    // 与设备交互-----------------------------------------------------------------------------------------
+    // 请求扫码
+    public boolean readScanFeeding(){
+        if(isLocal == 1){
+            return true;
+        }else{
+            return readBoolean(plcDb+".0.0");  //7002 扫码
+        }
+    }
+    // 扫码完成
+    public boolean readScanFinish(){
+        if(isLocal == 1){
+            return false;
+        }else{
+            return readBoolean(plcDb+".0.1");   //扫码完成
+        }
+    }
+
+    // 扫码完成反馈
+    public boolean writeScanFinish(boolean bt){
+        if(isLocal == 1){
+            return false;
+        }else{
+            return writeBoolean(plcDb+".0.1",bt);
+        }
+    }
+
+    // 读取中转车上料到设备
+    public boolean readFeedingTransferToDevice(){
+        if(isLocal == 1){
+            return true;
+        }else{
+            return readBoolean(plcDb+".0.2");
+        }
+    }
+
+    // 读取中转车号
+    public int readTransferNo(){
+        if(isLocal == 1){
+            return 1;
+        }else{
+            return s7PLC.readInt16(plcDb+".2.0");
+        }
+    }
+
+    // 读取中转车层数
+    public int readTransferFloor(){
+        if(isLocal == 1){
+            return 1;
+        }else{
+            return s7PLC.readInt16(plcDb+".4.0");
+        }
+    }
+
+    // 控制扫码枪读取二维码
+    public String readQrcode(){
+//        String sn = "5001040043561010006";
+        String sn = "";
+        if(isLocal == 1){
+            return "";
+        }else{
+            try{
+                if(this.pos.equals("A")){
+                    sn = MesClient.scanQrcode1();
+                }else{
+                    sn = MesClient.scanQrcode2();
+                }
+            }catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        return sn;
+    }
+
+    public boolean readBoolean(String db){
+        boolean permit;
+        try {
+            permit = s7PLC.readBoolean(db);
+//            log.info("<"+this.oprno+this.pos+">读取"+db+"-->成功,结果:{}", permit ? "true" : "false");
+        } catch (Exception e) {
+            log.error("<"+this.oprno+this.pos+">读取"+db+"-->失败", e);
+            return false;
+        }
+        return permit;
+    }
+
+    public boolean writeBoolean(String db, boolean permit){
+        try {
+            s7PLC.writeBoolean(db,permit);
+//            log.info("<"+this.oprno+this.pos+">写入"+db+"-->成功,写入内容:{}", permit ? "true" : "false");
+        } catch (Exception e) {
+//            log.error("<"+this.oprno+this.pos+">写入"+db+"-->失败,写入内容:{}", permit ? "true" : "false");
+            log.error(e.getMessage(), e);
+            return false;
+        }
+        return true;
+    }
+}

+ 533 - 0
src/com/mes/test/AlarmReadTest.java

@@ -0,0 +1,533 @@
+package com.mes.test;
+
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.sdk.client.api.identity.UsernameProvider;
+import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
+import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Scanner;
+
+/**
+ * 报警信息读取测试类
+ * 用于测试OP040设备的报警和刀具信息读取
+ * 
+ * @author MES
+ */
+public class AlarmReadTest {
+    
+    private static final Logger log = LoggerFactory.getLogger(AlarmReadTest.class);
+    
+    // 所有可用设备列表
+    private static final String[][] DEVICES = {
+        // 西侧设备 (80.22.10.x)
+        {"OP040A", "opc.tcp://80.22.10.42:4840"},
+        {"OP040B", "opc.tcp://80.22.10.43:4840"},
+        {"OP040C", "opc.tcp://80.22.10.44:4840"},
+        {"OP040D", "opc.tcp://80.22.10.45:4840"},
+        {"OP040E", "opc.tcp://80.22.10.46:4840"},
+        {"OP040F", "opc.tcp://80.22.10.47:4840"},
+        // 东侧设备 (80.23.10.x)
+        {"OP040G", "opc.tcp://80.23.10.42:4840"},
+        {"OP040H", "opc.tcp://80.23.10.43:4840"},
+        {"OP040I", "opc.tcp://80.23.10.44:4840"},
+        {"OP040J", "opc.tcp://80.23.10.45:4840"},
+        {"OP040K", "opc.tcp://80.23.10.46:4840"},
+        {"OP040L", "opc.tcp://80.23.10.47:4840"},
+    };
+    
+    private static final String USERNAME = "OPCUA";
+    private static final String PASSWORD = "OPCUA123";
+    
+    // 当前选择的设备
+    private String deviceName;
+    private String endpointUrl;
+    
+    // 命名空间索引
+    private static final int NS = 2;
+    
+    private OpcUaClient client;
+    
+    public static void main(String[] args) {
+        AlarmReadTest test = new AlarmReadTest();
+        test.run();
+    }
+    
+    /**
+     * 主运行方法 - 交互式选择设备
+     */
+    public void run() {
+        Scanner scanner = new Scanner(System.in);
+        
+        while (true) {
+            // 显示设备列表
+            printDeviceMenu();
+            
+            System.out.print("请输入设备编号 (1-12), 输入0退出: ");
+            String input = scanner.nextLine().trim();
+            
+            if ("0".equals(input) || "q".equalsIgnoreCase(input) || "quit".equalsIgnoreCase(input)) {
+                System.out.println("退出测试程序");
+                break;
+            }
+            
+            try {
+                int choice = Integer.parseInt(input);
+                if (choice < 1 || choice > 12) {
+                    System.out.println("无效输入,请输入1-12之间的数字");
+                    continue;
+                }
+                
+                // 选择设备 (用户输入1-12,数组索引0-11)
+                int deviceIndex = choice - 1;
+                deviceName = DEVICES[deviceIndex][0];
+                endpointUrl = DEVICES[deviceIndex][1];
+                
+                System.out.println("\n选择设备: " + deviceName + " (" + endpointUrl + ")");
+                
+                // 运行测试
+                runTest();
+                
+                System.out.println("\n按回车继续选择其他设备...");
+                scanner.nextLine();
+                
+            } catch (NumberFormatException e) {
+                System.out.println("无效输入,请输入数字");
+            }
+        }
+        
+        scanner.close();
+    }
+    
+    /**
+     * 打印设备菜单
+     */
+    private void printDeviceMenu() {
+        System.out.println("\n╔════════════════════════════════════════════════════════╗");
+        System.out.println("║          CNC设备报警/刀具信息测试工具                    ║");
+        System.out.println("╠════════════════════════════════════════════════════════╣");
+        System.out.println("║  西侧设备 (80.22.10.x)                                   ║");
+        System.out.println("║    1. OP040A (80.22.10.42)                              ║");
+        System.out.println("║    2. OP040B (80.22.10.43)                              ║");
+        System.out.println("║    3. OP040C (80.22.10.44)                              ║");
+        System.out.println("║    4. OP040D (80.22.10.45)                              ║");
+        System.out.println("║    5. OP040E (80.22.10.46)                              ║");
+        System.out.println("║    6. OP040F (80.22.10.47)                              ║");
+        System.out.println("╠════════════════════════════════════════════════════════╣");
+        System.out.println("║  东侧设备 (80.23.10.x)                                   ║");
+        System.out.println("║    7. OP040G (80.23.10.42)                              ║");
+        System.out.println("║    8. OP040H (80.23.10.43)                              ║");
+        System.out.println("║    9. OP040I (80.23.10.44)                              ║");
+        System.out.println("║   10. OP040J (80.23.10.45)                              ║");
+        System.out.println("║   11. OP040K (80.23.10.46)                              ║");
+        System.out.println("║   12. OP040L (80.23.10.47)                              ║");
+        System.out.println("╠════════════════════════════════════════════════════════╣");
+        System.out.println("║    0. 退出程序                                          ║");
+        System.out.println("╚════════════════════════════════════════════════════════╝");
+    }
+    
+    /**
+     * 运行测试
+     */
+    public void runTest() {
+        try {
+            // 1. 连接设备
+            if (!connect()) {
+                log.error("连接设备失败,测试终止");
+                return;
+            }
+            
+            log.info("========================================");
+            log.info("开始测试 {} ({}) 报警/刀具信息读取", deviceName, endpointUrl);
+            log.info("========================================");
+            
+            // 2. 先读取报警数量
+            testAlarmCount();
+            
+            // 3. 测试各种报警路径格式
+            testAlarmPaths();
+            
+            // 4. 遍历读取报警信息(多种索引格式)
+            testAlarmWithDifferentIndexes();
+            
+            // 5. 测试其他可能的报警相关路径
+            testOtherAlarmPaths();
+            
+            // 6. 测试刀具信息读取
+            testToolInfo();
+            
+        } catch (Exception e) {
+            log.error("测试过程中发生异常", e);
+        } finally {
+            disconnect();
+        }
+    }
+    
+    /**
+     * 连接到OPC UA服务器
+     */
+    private boolean connect() {
+        try {
+            log.info("正在连接到 {} ...", endpointUrl);
+            client = OpcUaClient.create(endpointUrl,
+                    endpoints -> endpoints.stream()
+                            .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getUri()))
+                            .findFirst(),
+                    configBuilder -> configBuilder
+                            .setIdentityProvider(new UsernameProvider(USERNAME, PASSWORD))
+                            .setRequestTimeout(UInteger.valueOf(10000))
+                            .build()
+            );
+            client.connect().get();
+            log.info("连接成功!");
+            return true;
+        } catch (Exception e) {
+            log.error("连接失败: {}", e.getMessage());
+            return false;
+        }
+    }
+    
+    /**
+     * 断开连接
+     */
+    private void disconnect() {
+        if (client != null) {
+            try {
+                client.disconnect().get();
+                log.info("已断开连接");
+            } catch (Exception e) {
+                log.error("断开连接异常", e);
+            }
+        }
+    }
+    
+    /**
+     * 读取单个变量
+     */
+    private DataValue readValue(String path) {
+        try {
+            NodeId nodeId = new NodeId(NS, path);
+            return client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
+        } catch (Exception e) {
+            log.debug("读取 {} 异常: {}", path, e.getMessage());
+            return null;
+        }
+    }
+    
+    /**
+     * 读取并打印结果
+     */
+    private void readAndPrint(String description, String path) {
+        DataValue dv = readValue(path);
+        if (dv != null) {
+            StatusCode status = dv.getStatusCode();
+            if (status != null && status.isGood()) {
+                Object value = dv.getValue().getValue();
+                log.info("[成功] {} - 路径: {} - 值: {} (类型: {})", 
+                        description, path, value, value != null ? value.getClass().getSimpleName() : "null");
+            } else {
+                log.warn("[失败] {} - 路径: {} - 状态: {}", description, path, status);
+            }
+        } else {
+            log.warn("[失败] {} - 路径: {} - 无法读取", description, path);
+        }
+    }
+    
+    /**
+     * 测试报警数量
+     */
+    private void testAlarmCount() {
+        log.info("\n---------- 测试报警数量 ----------");
+        readAndPrint("报警数量", "/Nck/State/numAlarms");
+    }
+    
+    /**
+     * 测试各种报警路径格式
+     */
+    private void testAlarmPaths() {
+        log.info("\n---------- 测试报警路径格式 ----------");
+        
+        // 报警类型 clearInfo
+        String[] clearInfoPaths = {
+            "/Nck/SequencedAlarms/clearInfo[0]",
+            "/Nck/SequencedAlarms/clearInfo[1]",
+            "/Nck/SequencedAlarms/clearInfo[u1]",
+            "/Nck/SequencedAlarms/clearInfo",
+        };
+        
+        for (String path : clearInfoPaths) {
+            readAndPrint("报警类型 clearInfo", path);
+        }
+        
+        // 报警号 textIndex
+        String[] textIndexPaths = {
+            "/Nck/SequencedAlarms/textIndex[0]",
+            "/Nck/SequencedAlarms/textIndex[1]",
+            "/Nck/SequencedAlarms/textIndex[u1]",
+            "/Nck/SequencedAlarms/textIndex",
+        };
+        
+        for (String path : textIndexPaths) {
+            readAndPrint("报警号 textIndex", path);
+        }
+    }
+    
+    /**
+     * 测试不同索引格式的报警读取
+     */
+    private void testAlarmWithDifferentIndexes() {
+        log.info("\n---------- 遍历报警索引 (0-10) ----------");
+        
+        // 遍历索引0-10,使用不同格式
+        for (int i = 0; i <= 10; i++) {
+            // 格式1: [index]
+            String path1 = "/Nck/SequencedAlarms/clearInfo[" + i + "]";
+            String path2 = "/Nck/SequencedAlarms/textIndex[" + i + "]";
+            readAndPrint("clearInfo[" + i + "]", path1);
+            readAndPrint("textIndex[" + i + "]", path2);
+            
+            // 格式2: [uIndex]
+            String path3 = "/Nck/SequencedAlarms/clearInfo[u" + i + "]";
+            String path4 = "/Nck/SequencedAlarms/textIndex[u" + i + "]";
+            readAndPrint("clearInfo[u" + i + "]", path3);
+            readAndPrint("textIndex[u" + i + "]", path4);
+        }
+        
+        log.info("\n---------- 测试双索引格式 ----------");
+        // 尝试双索引格式 [channel, index]
+        for (int ch = 1; ch <= 2; ch++) {
+            for (int idx = 0; idx <= 3; idx++) {
+                String path1 = "/Nck/SequencedAlarms/clearInfo[u" + ch + "," + idx + "]";
+                String path2 = "/Nck/SequencedAlarms/textIndex[u" + ch + "," + idx + "]";
+                readAndPrint("clearInfo[u" + ch + "," + idx + "]", path1);
+                readAndPrint("textIndex[u" + ch + "," + idx + "]", path2);
+            }
+        }
+    }
+    
+    /**
+     * 测试其他可能的报警相关路径
+     */
+    private void testOtherAlarmPaths() {
+        log.info("\n---------- 测试其他报警相关路径 ----------");
+        
+        // 可能的报警信息路径
+        String[] otherPaths = {
+            // Alarm相关
+            "/Nck/Alarms/numAlarms",
+            "/Nck/Alarm/numAlarms",
+            "/Nck/AlarmList/numAlarms",
+            
+            // SequencedAlarms 其他属性
+            "/Nck/SequencedAlarms/alarmNo[0]",
+            "/Nck/SequencedAlarms/alarmNo[1]",
+            "/Nck/SequencedAlarms/alarmText[0]",
+            "/Nck/SequencedAlarms/alarmText[1]",
+            "/Nck/SequencedAlarms/alarmMsg[0]",
+            "/Nck/SequencedAlarms/alarmMsg[1]",
+            "/Nck/SequencedAlarms/message[0]",
+            "/Nck/SequencedAlarms/message[1]",
+            "/Nck/SequencedAlarms/text[0]",
+            "/Nck/SequencedAlarms/text[1]",
+            "/Nck/SequencedAlarms/description[0]",
+            "/Nck/SequencedAlarms/description[1]",
+            
+            // 报警时间
+            "/Nck/SequencedAlarms/alarmTime[0]",
+            "/Nck/SequencedAlarms/alarmTime[1]",
+            "/Nck/SequencedAlarms/time[0]",
+            "/Nck/SequencedAlarms/time[1]",
+            "/Nck/SequencedAlarms/timestamp[0]",
+            "/Nck/SequencedAlarms/timestamp[1]",
+            
+            // 报警状态
+            "/Nck/SequencedAlarms/state[0]",
+            "/Nck/SequencedAlarms/state[1]",
+            "/Nck/SequencedAlarms/status[0]",
+            "/Nck/SequencedAlarms/status[1]",
+            "/Nck/SequencedAlarms/active[0]",
+            "/Nck/SequencedAlarms/active[1]",
+            
+            // 报警号/ID
+            "/Nck/SequencedAlarms/id[0]",
+            "/Nck/SequencedAlarms/id[1]",
+            "/Nck/SequencedAlarms/number[0]",
+            "/Nck/SequencedAlarms/number[1]",
+            
+            // Channel级别的报警
+            "/Channel/State/numAlarms",
+            "/Channel/State/numAlarms[u1]",
+            "/Channel/Alarms/numAlarms",
+            "/Channel/Alarms/numAlarms[u1]",
+            
+            // 带通道的SequencedAlarms
+            "/Channel/SequencedAlarms/clearInfo[u1,0]",
+            "/Channel/SequencedAlarms/textIndex[u1,0]",
+            
+            // 活动报警
+            "/Nck/State/activeAlarms",
+            "/Nck/State/activeAlarm",
+            "/Nck/ActiveAlarms/numAlarms",
+        };
+        
+        for (String path : otherPaths) {
+            readAndPrint("探索路径", path);
+        }
+        
+        log.info("\n---------- 测试不同命名空间 ----------");
+        // 尝试不同的命名空间
+        String[] testPathsForNs = {
+            "/Nck/State/numAlarms",
+            "/Nck/SequencedAlarms/clearInfo[0]",
+            "/Nck/SequencedAlarms/textIndex[0]",
+        };
+        
+        for (int ns = 0; ns <= 4; ns++) {
+            log.info("--- 命名空间 ns={} ---", ns);
+            for (String path : testPathsForNs) {
+                try {
+                    NodeId nodeId = new NodeId(ns, path);
+                    DataValue dv = client.readValue(0.0, TimestampsToReturn.Both, nodeId).get();
+                    if (dv != null) {
+                        StatusCode status = dv.getStatusCode();
+                        if (status != null && status.isGood()) {
+                            Object value = dv.getValue().getValue();
+                            log.info("[成功] ns={} path={} 值={}", ns, path, value);
+                        } else {
+                            log.debug("[失败] ns={} path={} 状态={}", ns, path, status);
+                        }
+                    }
+                } catch (Exception e) {
+                    log.debug("[异常] ns={} path={} 错误={}", ns, path, e.getMessage());
+                }
+            }
+        }
+    }
+    
+    /**
+     * 测试刀具信息读取
+     * 根据文档:
+     * - /Tool/ActiveCatalogue/toolIdent[u<Area index>, <Row index>] - 刀具名称
+     * - /Tool/ActiveCatalogue/toolNo[u<Area index>, <Row index>] - 刀号
+     * 
+     * OP40刀具信息:
+     * 刀号1: HM铝用平头铣刀 D8.8x35x130xSD8.6,3T (420件)
+     * 刀号2: HM铝用平头铣刀 D10.0x30x75xSD10,3T (2300件)
+     * 刀号3: HM铝用平头铣刀 D16.0x45x100xSD16,3T (4000件)
+     * 刀号4: 锯片铣刀片 D150-H32-H5-Z72 (600件)
+     */
+    private void testToolInfo() {
+        log.info("\n========================================");
+        log.info("测试刀具信息读取");
+        log.info("========================================");
+        
+        // 测试基本刀具路径
+        log.info("\n---------- 测试基本刀具路径 ----------");
+        String[] basicToolPaths = {
+            "/Tool/ActiveCatalogue/toolIdent",
+            "/Tool/ActiveCatalogue/toolNo",
+            "/Tool/Catalogue/toolIdent",
+            "/Tool/Catalogue/toolNo",
+        };
+        for (String path : basicToolPaths) {
+            readAndPrint("刀具基本路径", path);
+        }
+        
+        // 测试单索引格式 [index]
+        log.info("\n---------- 测试刀具单索引格式 [index] ----------");
+        for (int i = 0; i <= 5; i++) {
+            readAndPrint("toolIdent[" + i + "]", "/Tool/ActiveCatalogue/toolIdent[" + i + "]");
+            readAndPrint("toolNo[" + i + "]", "/Tool/ActiveCatalogue/toolNo[" + i + "]");
+        }
+        
+        // 测试单索引格式 [uIndex]
+        log.info("\n---------- 测试刀具单索引格式 [uIndex] ----------");
+        for (int i = 1; i <= 5; i++) {
+            readAndPrint("toolIdent[u" + i + "]", "/Tool/ActiveCatalogue/toolIdent[u" + i + "]");
+            readAndPrint("toolNo[u" + i + "]", "/Tool/ActiveCatalogue/toolNo[u" + i + "]");
+        }
+        
+        // 测试双索引格式 [uArea, Row] - 这是文档推荐的格式
+        log.info("\n---------- 测试刀具双索引格式 [uArea, Row] ----------");
+        // Area index 通常是刀具区域, Row index 是刀沿序号
+        for (int area = 1; area <= 2; area++) {
+            for (int row = 1; row <= 5; row++) {
+                String identPath = "/Tool/ActiveCatalogue/toolIdent[u" + area + "," + row + "]";
+                String noPath = "/Tool/ActiveCatalogue/toolNo[u" + area + "," + row + "]";
+                readAndPrint("toolIdent[u" + area + "," + row + "]", identPath);
+                readAndPrint("toolNo[u" + area + "," + row + "]", noPath);
+            }
+        }
+        
+        // 测试从0开始的行索引
+        log.info("\n---------- 测试刀具双索引格式 (行索引从0开始) ----------");
+        for (int area = 1; area <= 2; area++) {
+            for (int row = 0; row <= 4; row++) {
+                String identPath = "/Tool/ActiveCatalogue/toolIdent[u" + area + "," + row + "]";
+                String noPath = "/Tool/ActiveCatalogue/toolNo[u" + area + "," + row + "]";
+                readAndPrint("toolIdent[u" + area + "," + row + "]", identPath);
+                readAndPrint("toolNo[u" + area + "," + row + "]", noPath);
+            }
+        }
+        
+        // 测试其他可能的刀具相关路径
+        log.info("\n---------- 测试其他刀具相关路径 ----------");
+        String[] otherToolPaths = {
+            // 当前刀具
+            "/Channel/State/actTNumber[u1]",
+            "/Channel/State/actToolIdent[u1]",
+            "/Channel/State/curTool[u1]",
+            "/Channel/State/currentTool[u1]",
+            "/Channel/Tool/actTNumber[u1]",
+            "/Channel/Tool/actToolIdent[u1]",
+            
+            // 刀具管理
+            "/Tool/Management/actTNumber",
+            "/Tool/Management/actToolIdent",
+            "/Tool/State/actTNumber",
+            "/Tool/State/actToolIdent",
+            
+            // 刀具补偿
+            "/Tool/Compensation/toolIdent[u1,1]",
+            "/Tool/Compensation/toolNo[u1,1]",
+            
+            // 刀库
+            "/Tool/Magazine/toolIdent[u1,1]",
+            "/Tool/Magazine/toolNo[u1,1]",
+            
+            // 主轴上的刀具
+            "/Tool/Spindle/toolIdent[u1]",
+            "/Tool/Spindle/toolNo[u1]",
+            "/Spindle/Tool/toolIdent[u1]",
+            "/Spindle/Tool/toolNo[u1]",
+        };
+        
+        for (String path : otherToolPaths) {
+            readAndPrint("其他刀具路径", path);
+        }
+        
+        // 测试刀具寿命相关路径
+        log.info("\n---------- 测试刀具寿命相关路径 ----------");
+        String[] toolLifePaths = {
+            "/Tool/ActiveCatalogue/toolLife[u1,1]",
+            "/Tool/ActiveCatalogue/toolLife[u1,2]",
+            "/Tool/ActiveCatalogue/toolLife[u1,3]",
+            "/Tool/ActiveCatalogue/toolLife[u1,4]",
+            "/Tool/ActiveCatalogue/usedLife[u1,1]",
+            "/Tool/ActiveCatalogue/remainLife[u1,1]",
+            "/Tool/Catalogue/toolLife[u1,1]",
+            "/Tool/Management/toolLife[1]",
+            "/Tool/Management/usedPieces[1]",
+            "/Tool/Management/remainPieces[1]",
+        };
+        
+        for (String path : toolLifePaths) {
+            readAndPrint("刀具寿命路径", path);
+        }
+    }
+}

+ 495 - 0
src/com/mes/test/ToolLifeTest.java

@@ -0,0 +1,495 @@
+package com.mes.test;
+
+import com.mes.util.OpcUaUtils;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
+import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
+
+import java.util.Scanner;
+
+/**
+ * 刀具寿命采集测试类
+ * 用于测试从西门子CNC系统采集刀具寿命数据
+ * 
+ * 采集参数:
+ * - $TC_MOP4[T,D] - 剩余工件数
+ * - $TC_MOP3[T,D] - 预警阈值
+ * - $TC_MOP13[T,D] - 目标寿命值
+ */
+public class ToolLifeTest {
+    
+    private OpcUaUtils opcUaClient;
+    
+    public ToolLifeTest(String endpointUrl, String username, String password) {
+        this.opcUaClient = new OpcUaUtils(endpointUrl, username, password);
+    }
+    
+    /**
+     * 连接到OPC UA服务器
+     */
+    public boolean connect() {
+        System.out.println("正在连接到CNC设备...");
+        boolean connected = opcUaClient.connect();
+        if (connected) {
+            System.out.println("✓ 连接成功");
+        } else {
+            System.out.println("✗ 连接失败");
+        }
+        return connected;
+    }
+    
+    /**
+     * 读取刀具寿命数据
+     * @param dNumber D号(刀具号)
+     * @param hNumber H号(刀沿号)
+     */
+    public void readToolLifeData(int dNumber, int hNumber) {
+        System.out.println("\n========================================");
+        System.out.println("刀具信息: D" + dNumber + " H" + hNumber);
+        System.out.println("========================================");
+        
+        try {
+            // 读取剩余工件数 $TC_MOP4[D,H]
+            Integer remainingLife = readToolParameter("TC_MOP4", dNumber, hNumber);
+            
+            // 读取预警阈值 $TC_MOP3[D,H]
+            Integer warningThreshold = readToolParameter("TC_MOP3", dNumber, hNumber);
+            
+            // 读取目标寿命值 $TC_MOP13[D,H]
+            Integer targetLife = readToolParameter("TC_MOP13", dNumber, hNumber);
+            
+            // 显示结果
+            System.out.println("\n采集结果:");
+            System.out.println("----------------------------------------");
+            System.out.println("剩余工件数 ($TC_MOP4):  " + (remainingLife != null ? remainingLife : "读取失败"));
+            System.out.println("预警阈值   ($TC_MOP3):  " + (warningThreshold != null ? warningThreshold : "读取失败"));
+            System.out.println("目标寿命   ($TC_MOP13): " + (targetLife != null ? targetLife : "读取失败"));
+            
+            // 计算寿命百分比和状态
+            if (remainingLife != null && targetLife != null && targetLife > 0) {
+                double lifePercentage = (remainingLife * 100.0) / targetLife;
+                System.out.println("寿命百分比: " + String.format("%.2f%%", lifePercentage));
+                
+                // 判断状态
+                String status;
+                if (remainingLife == 0) {
+                    status = "❌ 寿命耗尽";
+                } else if (warningThreshold != null && remainingLife <= warningThreshold) {
+                    status = "⚠️  预警状态";
+                } else {
+                    status = "✓ 正常";
+                }
+                System.out.println("刀具状态:   " + status);
+            }
+            System.out.println("----------------------------------------\n");
+            
+        } catch (Exception e) {
+            System.err.println("读取刀具数据时发生错误: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+    
+    /**
+     * 读取刀具参数
+     * @param paramName 参数名称 (TC_MOP3, TC_MOP4, TC_MOP13)
+     * @param dNumber D号(刀具号)
+     * @param hNumber H号(刀沿号)- 暂时未使用
+     * @return 参数值
+     */
+    private Integer readToolParameter(String paramName, int dNumber, int hNumber) {
+        // 根据测试结果,索引映射关系:
+        // $TC_MOP4[T,D] -> /Tool/Supervision/data[u1,cT,4] (剩余工件数)
+        // $TC_MOP3[T,D] -> /Tool/Supervision/data[u1,cT,3] (预警阈值)
+        // $TC_MOP13[T,D] -> /Tool/Supervision/data[u1,cT,6] (目标寿命)
+        
+        int index;
+        if (paramName.equals("TC_MOP4")) {
+            index = 4; // 剩余工件数
+        } else if (paramName.equals("TC_MOP3")) {
+            index = 3; // 预警阈值
+        } else if (paramName.equals("TC_MOP13")) {
+            index = 6; // 目标寿命
+        } else {
+            System.out.println("  ⚠️  未知参数: " + paramName);
+            return null;
+        }
+        
+        String path = "/Tool/Supervision/data[u1,c" + dNumber + "," + index + "]";
+        
+        try {
+            System.out.println("读取路径: " + path);
+            DataValue dataValue = opcUaClient.readValue(path);
+            
+            if (dataValue != null && dataValue.getValue() != null && dataValue.getValue().getValue() != null) {
+                Object value = dataValue.getValue().getValue();
+                if (value instanceof Number) {
+                    Integer result = ((Number) value).intValue();
+                    System.out.println("  ✓ 读取成功: " + result);
+                    return result;
+                }
+            }
+        } catch (Exception e) {
+            System.out.println("  ✗ 读取失败: " + e.getMessage());
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 测试读取Tool/Supervision/data的不同索引
+     */
+    public void testToolSupervisionData(int dNumber) {
+        System.out.println("\n========================================");
+        System.out.println("测试 /Tool/Supervision/data 不同索引 (D" + dNumber + ")");
+        System.out.println("========================================\n");
+        
+        // 测试索引 1-20
+        for (int i = 1; i <= 20; i++) {
+            String path = "/Tool/Supervision/data[u1,c" + dNumber + "," + i + "]";
+            try {
+                DataValue dataValue = opcUaClient.readValue(path);
+                if (dataValue != null && dataValue.getValue() != null && dataValue.getValue().getValue() != null) {
+                    Object value = dataValue.getValue().getValue();
+                    System.out.println("索引 " + i + ": " + value);
+                }
+            } catch (Exception e) {
+                // 忽略错误
+            }
+        }
+    }
+    
+    /**
+     * 查找刀具寿命三个参数的索引映射
+     * 目标:找到哪个索引对应 $TC_MOP4、$TC_MOP3、$TC_MOP13
+     */
+    public void findToolLifeIndexes(int dNumber) {
+        System.out.println("\n========================================");
+        System.out.println("查找刀具寿命参数索引映射 (D" + dNumber + ")");
+        System.out.println("========================================");
+        System.out.println("目标值:");
+        System.out.println("  $TC_MOP4[" + dNumber + ",1] - 剩余工件数(期望:2300左右)");
+        System.out.println("  $TC_MOP3[" + dNumber + ",1] - 预警阈值(期望:20左右)");
+        System.out.println("  $TC_MOP13[" + dNumber + ",1] - 目标寿命(期望:2280左右)");
+        System.out.println("\n开始扫描索引 1-30...\n");
+        
+        // 扫描索引 1-30
+        for (int i = 1; i <= 30; i++) {
+            String path = "/Tool/Supervision/data[u1,c" + dNumber + "," + i + "]";
+            try {
+                DataValue dataValue = opcUaClient.readValue(path);
+                if (dataValue != null && dataValue.getValue() != null && dataValue.getValue().getValue() != null) {
+                    Object value = dataValue.getValue().getValue();
+                    
+                    // 转换为数字
+                    if (value instanceof Number) {
+                        int intValue = ((Number) value).intValue();
+                        
+                        // 判断可能对应的参数
+                        String guess = "";
+                        if (intValue > 1000 && intValue < 3000) {
+                            guess = " <- 可能是 $TC_MOP4 (剩余工件数) 或 $TC_MOP13 (目标寿命)";
+                        } else if (intValue > 10 && intValue < 100) {
+                            guess = " <- 可能是 $TC_MOP3 (预警阈值)";
+                        } else if (intValue == 0) {
+                            continue; // 跳过0值
+                        }
+                        
+                        System.out.println("索引 " + String.format("%2d", i) + ": " + String.format("%6d", intValue) + guess);
+                    }
+                }
+            } catch (Exception e) {
+                // 忽略错误
+            }
+        }
+        
+        System.out.println("\n========================================");
+        System.out.println("扫描完成!请根据上面的值判断索引映射关系");
+        System.out.println("========================================");
+    }
+    
+    /**
+     * 浏览OPC UA节点树,查找刀具相关节点
+     */
+    public void browseToolNodes() {
+        System.out.println("\n========================================");
+        System.out.println("浏览OPC UA节点结构");
+        System.out.println("========================================\n");
+        
+        String[] basePaths = {
+            "/Tool",
+            "/Nck",
+            "/Channel",
+            "/Tool/Supervision",
+            "/Tool/Data"
+        };
+        
+        for (String basePath : basePaths) {
+            System.out.println("\n检查路径: " + basePath);
+            try {
+                // 尝试读取该路径
+                DataValue dataValue = opcUaClient.readValue(basePath);
+                if (dataValue != null) {
+                    System.out.println("  ✓ 路径存在");
+                }
+            } catch (Exception e) {
+                System.out.println("  ✗ 路径不存在或无法访问: " + e.getMessage());
+            }
+        }
+    }
+    
+    /**
+     * 批量读取多个刀具的寿命数据
+     */
+    public void readMultipleTools(int[] dNumbers, int[] hNumbers) {
+        if (dNumbers.length != hNumbers.length) {
+            System.err.println("D号和H号数量不匹配");
+            return;
+        }
+        
+        for (int i = 0; i < dNumbers.length; i++) {
+            readToolLifeData(dNumbers[i], hNumbers[i]);
+            
+            // 间隔一下,避免请求过快
+            try {
+                Thread.sleep(500);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+    
+    /**
+     * 测试自定义路径(支持指定命名空间)
+     */
+    public void testCustomPath(String path) {
+        System.out.println("\n测试路径: " + path);
+        
+        // 尝试不同的命名空间索引 (ns=2, ns=3, ns=4)
+        int[] namespaces = {2, 3, 4, 5};
+        
+        for (int ns : namespaces) {
+            try {
+                System.out.println("尝试命名空间 ns=" + ns);
+                NodeId nodeId = new NodeId(ns, path);
+                DataValue dataValue = opcUaClient.getClient().readValue(0.0, TimestampsToReturn.Both, nodeId).get();
+                
+                if (dataValue != null && dataValue.getValue() != null && dataValue.getValue().getValue() != null) {
+                    Object value = dataValue.getValue().getValue();
+                    System.out.println("✓ 读取成功 (ns=" + ns + ")");
+                    System.out.println("  值: " + value);
+                    System.out.println("  类型: " + value.getClass().getSimpleName());
+                    return;
+                } else {
+                    System.out.println("  状态: " + (dataValue != null ? dataValue.getStatusCode() : "null"));
+                }
+            } catch (Exception e) {
+                System.out.println("  失败: " + e.getMessage());
+            }
+        }
+        
+        System.out.println("✗ 所有命名空间都读取失败");
+    }
+    
+    /**
+     * 获取OPC UA客户端实例(用于测试)
+     */
+    public OpcUaUtils getClient() {
+        return opcUaClient;
+    }
+    
+    /**
+     * 批量测试多个刀具的寿命数据(1-8号和20号刀)
+     */
+    public void testMultipleToolsLife() {
+        System.out.println("\n========================================");
+        System.out.println("批量测试刀具寿命(1-8号和20号刀)");
+        System.out.println("========================================\n");
+        
+        // 测试刀具号:1, 2, 3, 4, 5, 6, 7, 8, 20
+        int[] toolNumbers = {1, 2, 3, 4, 5, 6, 7, 8, 20};
+        
+        for (int toolNo : toolNumbers) {
+            System.out.println("\n----------------------------------------");
+            System.out.println("刀具 T" + toolNo + "D1 的寿命数据:");
+            System.out.println("----------------------------------------");
+            
+            try {
+                // 读取三个参数
+                Integer mop4 = readToolLifeParameter(toolNo, 4);  // 剩余工件数
+                Integer mop3 = readToolLifeParameter(toolNo, 3);  // 预警阈值
+                Integer mop13 = readToolLifeParameter(toolNo, 6); // 目标寿命
+                
+                // 显示结果
+                System.out.println("剩余工件数 ($TC_MOP4):  " + (mop4 != null ? mop4 : "读取失败"));
+                System.out.println("预警阈值   ($TC_MOP3):  " + (mop3 != null ? mop3 : "读取失败"));
+                System.out.println("目标寿命   ($TC_MOP13): " + (mop13 != null ? mop13 : "读取失败"));
+                
+                // 计算寿命百分比和状态
+                if (mop4 != null && mop13 != null && mop13 > 0) {
+                    double lifePercentage = (mop4 * 100.0) / mop13;
+                    System.out.println("寿命百分比: " + String.format("%.2f%%", lifePercentage));
+                    
+                    // 判断状态
+                    String status;
+                    if (mop4 == 0) {
+                        status = "❌ 寿命耗尽";
+                    } else if (mop3 != null && mop4 <= mop3) {
+                        status = "⚠️  预警状态";
+                    } else {
+                        status = "✓ 正常";
+                    }
+                    System.out.println("刀具状态:   " + status);
+                }
+                
+                // 间隔一下
+                Thread.sleep(300);
+                
+            } catch (Exception e) {
+                System.err.println("读取刀具 T" + toolNo + " 数据时发生错误: " + e.getMessage());
+            }
+        }
+        
+        System.out.println("\n========================================");
+        System.out.println("批量测试完成!");
+        System.out.println("========================================");
+    }
+    
+    /**
+     * 读取刀具寿命参数(简化版,不打印详细日志)
+     */
+    private Integer readToolLifeParameter(int toolNo, int index) {
+        String path = "/Tool/Supervision/data[u1,c" + toolNo + "," + index + "]";
+        
+        try {
+            DataValue dataValue = opcUaClient.readValue(path);
+            
+            if (dataValue != null && dataValue.getValue() != null && dataValue.getValue().getValue() != null) {
+                Object value = dataValue.getValue().getValue();
+                if (value instanceof Number) {
+                    return ((Number) value).intValue();
+                }
+            }
+        } catch (Exception e) {
+            // 忽略错误
+        }
+        
+        return null;
+    }
+    
+    /**
+     * 断开连接
+     */
+    public void disconnect() {
+        if (opcUaClient != null) {
+            opcUaClient.disconnect();
+            System.out.println("已断开连接");
+        }
+    }
+    
+    /**
+     * 主测试方法
+     */
+    public static void main(String[] args) {
+        Scanner scanner = new Scanner(System.in);
+        
+        // 配置连接参数
+        System.out.println("=== CNC刀具寿命采集测试 ===\n");
+        
+        System.out.print("请输入CNC设备IP地址 (默认: 80.23.10.45): ");
+        String ip = scanner.nextLine().trim();
+        if (ip.isEmpty()) {
+            ip = "80.23.10.45";
+        }
+        
+        String endpointUrl = "opc.tcp://" + ip + ":4840";
+        String username = "OPCUA";
+        String password = "OPCUA123";
+        
+        ToolLifeTest test = new ToolLifeTest(endpointUrl, username, password);
+        
+        // 连接到设备
+        if (!test.connect()) {
+            System.err.println("无法连接到设备,程序退出");
+            scanner.close();
+            return;
+        }
+        
+        // 交互式测试
+        while (true) {
+            System.out.println("\n请选择操作:");
+            System.out.println("1. 读取单个刀具寿命");
+            System.out.println("2. 批量读取刀具寿命");
+            System.out.println("3. 浏览OPC UA节点结构");
+            System.out.println("4. 测试已知路径");
+            System.out.println("5. 测试Tool/Supervision/data索引");
+            System.out.println("6. 测试刀具寿命三个参数(找索引映射)");
+            System.out.println("7. 批量测试多个刀具(1-8和20号刀)");
+            System.out.println("8. 退出");
+            System.out.print("请输入选项 (1-8): ");
+            
+            String choice = scanner.nextLine().trim();
+            
+            switch (choice) {
+                case "1":
+                    // 单个刀具测试
+                    System.out.print("请输入D号 (刀具号,例如: 1): ");
+                    int dNumber = Integer.parseInt(scanner.nextLine().trim());
+                    
+                    System.out.print("请输入H号 (刀沿号,例如: 1): ");
+                    int hNumber = Integer.parseInt(scanner.nextLine().trim());
+                    
+                    test.readToolLifeData(dNumber, hNumber);
+                    break;
+                    
+                case "2":
+                    // 批量测试 - 根据图片中的刀具表
+                    System.out.println("\n批量读取刀具寿命 (D1-D3, 各H1刀沿)...");
+                    int[] dNumbers = {1, 2, 3};
+                    int[] hNumbers = {1, 1, 1};
+                    test.readMultipleTools(dNumbers, hNumbers);
+                    break;
+                    
+                case "3":
+                    // 浏览节点
+                    test.browseToolNodes();
+                    break;
+                    
+                case "4":
+                    // 测试自定义路径
+                    System.out.print("请输入完整的OPC UA路径 (例如: /Tool/Supervision/data[u1,c1,3]): ");
+                    String customPath = scanner.nextLine().trim();
+                    test.testCustomPath(customPath);
+                    break;
+                    
+                case "5":
+                    // 测试Tool/Supervision/data索引
+                    System.out.print("请输入D号 (刀具号): ");
+                    int testD = Integer.parseInt(scanner.nextLine().trim());
+                    test.testToolSupervisionData(testD);
+                    break;
+                    
+                case "6":
+                    // 测试三个参数的索引映射
+                    System.out.print("请输入D号 (刀具号,例如: 3): ");
+                    int toolD = Integer.parseInt(scanner.nextLine().trim());
+                    test.findToolLifeIndexes(toolD);
+                    break;
+                    
+                case "7":
+                    // 批量测试1-8和20号刀
+                    test.testMultipleToolsLife();
+                    break;
+                    
+                case "8":
+                    // 退出
+                    test.disconnect();
+                    scanner.close();
+                    System.out.println("程序已退出");
+                    return;
+                    
+                default:
+                    System.out.println("无效选项,请重新输入");
+            }
+        }
+    }
+}

+ 58 - 0
src/com/mes/ui/BindMaterialResp.java

@@ -0,0 +1,58 @@
+package com.mes.ui;
+
+public class BindMaterialResp {
+    private String materialId;
+    private String materialTitle;
+    private String craft;
+    private String batchSn;
+    private String lastTimes;
+    private String type;
+
+    public String getMaterialId() {
+        return materialId;
+    }
+
+    public void setMaterialId(String materialId) {
+        this.materialId = materialId;
+    }
+
+    public String getMaterialTitle() {
+        return materialTitle;
+    }
+
+    public void setMaterialTitle(String materialTitle) {
+        this.materialTitle = materialTitle;
+    }
+
+    public String getCraft() {
+        return craft;
+    }
+
+    public void setCraft(String craft) {
+        this.craft = craft;
+    }
+
+    public String getBatchSn() {
+        return batchSn;
+    }
+
+    public void setBatchSn(String batchSn) {
+        this.batchSn = batchSn;
+    }
+
+    public String getLastTimes() {
+        return lastTimes;
+    }
+
+    public void setLastTimes(String lastTimes) {
+        this.lastTimes = lastTimes;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+}

+ 89 - 0
src/com/mes/ui/CacheData.java

@@ -0,0 +1,89 @@
+package com.mes.ui;
+
+public class CacheData {
+    private Integer tjFlag;
+    private String msg;
+    private String sn;
+    private String feedingSn;
+    private String lastSn;
+    private Integer error;
+    private Integer tjStatus;
+    private String preSn;
+
+    public Integer getTjFlag() {
+        return tjFlag;
+    }
+
+    public void setTjFlag(Integer tjFlag) {
+        this.tjFlag = tjFlag;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+    public Integer getError() {
+        return error;
+    }
+
+    public void setError(Integer error) {
+        this.error = error;
+    }
+
+    public String getLastSn() {
+        return lastSn;
+    }
+
+    public void setLastSn(String lastSn) {
+        this.lastSn = lastSn;
+    }
+
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    public Integer getTjStatus() {
+        return tjStatus;
+    }
+
+    public void setTjStatus(Integer tjStatus) {
+        this.tjStatus = tjStatus;
+    }
+
+    public String getFeedingSn() {
+        return feedingSn;
+    }
+
+    public void setFeedingSn(String feedingSn) {
+        this.feedingSn = feedingSn;
+    }
+
+    public String getPreSn() {
+        return preSn;
+    }
+
+    public void setPreSn(String preSn) {
+        this.preSn = preSn;
+    }
+
+    @Override
+    public String toString() {
+        return "CacheData{" +
+                "tjFlag=" + tjFlag +
+                ", msg='" + msg + '\'' +
+                ", sn='" + sn + '\'' +
+                ", feedingSn='" + feedingSn + '\'' +
+                ", lastSn='" + lastSn + '\'' +
+                ", error=" + error +
+                ", tjStatus=" + tjStatus +
+                '}';
+    }
+}

+ 71 - 0
src/com/mes/ui/CncParamReq.java

@@ -0,0 +1,71 @@
+package com.mes.ui;
+
+/**
+ * CNC过程参数请求类
+ * 用于存储从OPC UA读取的CNC参数,并上传到服务端
+ */
+public class CncParamReq {
+    private Integer id;
+    private String oprno;       // 工位号
+    private String lineSn;      // 产线编号
+    private String sn;          // 工件码
+    private String deviceCode;  // 设备编号
+    private String content;     // JSON格式的完整参数
+    private Integer sync;       // 同步状态 0=未同步 1=已同步
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getOprno() {
+        return oprno;
+    }
+
+    public void setOprno(String oprno) {
+        this.oprno = oprno;
+    }
+
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public void setLineSn(String lineSn) {
+        this.lineSn = lineSn;
+    }
+
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    public String getDeviceCode() {
+        return deviceCode;
+    }
+
+    public void setDeviceCode(String deviceCode) {
+        this.deviceCode = deviceCode;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Integer getSync() {
+        return sync;
+    }
+
+    public void setSync(Integer sync) {
+        this.sync = sync;
+    }
+}

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

@@ -0,0 +1,392 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.DateLocalUtils;
+import com.mes.util.JdbcUtils;
+import com.mes.util.PendingQualityData;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+public class DataUtil {
+
+    public static final Logger log =  LoggerFactory.getLogger(DataUtil.class);
+
+    public static JSONObject checkToResult(String sn,String oprno,String lineSn) {
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesProductRecord/checkToResult";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            return null;
+        }
+    }
+
+    public static JSONObject checkQuality(String sn,String oprno, String user){
+        return checkQuality(sn, oprno, user, "STATION");
+    }
+
+    /**
+     * 质量检查(带来源类型)
+     * @param sn 工件码
+     * @param oprno 工位号
+     * @param user 操作用户
+     * @param sourceType 来源类型:STATION=工位扫码, TRANSFER=中转车扫码
+     * @return 检查结果
+     */
+    public static JSONObject checkQuality(String sn, String oprno, String user, String sourceType){
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesProductRecord/pccheck";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn+"&ucode="+user;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                JdbcUtils.insertData(oprno, "100000", params, "AQDW", sn);
+                JSONObject retObj = JSONObject.parseObject(result);
+                // 质量检查通过后,保存到本地待提交表
+                if(retObj != null && retObj.get("result") != null 
+                        && retObj.get("result").toString().equalsIgnoreCase("true")) {
+                    JdbcUtils.insertPendingQuality(oprno, sn, user, sourceType);
+                }
+                return retObj;
+            }
+        }catch (Exception e){
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static JSONObject sendQuality(String sn,String oprno,String ret,String user){
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesProductRecord/pcresult";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn+"&result="+ret+"&ucode="+user;
+            log.info("params="+params);
+            
+            // 调用sendQuality即判定为已提交,从本地待提交表删除该码(不等服务器响应)
+            JdbcUtils.deletePendingQuality(sn);
+            
+            String result = doPost(url,params);
+            log.info("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                JdbcUtils.insertData(oprno, "100000", params, "MQDW", sn);
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static JSONObject getSn(String oprno,int transferSn,int transferFloor,String user){
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesTransfer/getSn";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&transferSn="+transferSn+"&transferFloor="+transferFloor+"&ucode="+user;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                JdbcUtils.insertData(oprno, "100000", params, "MQDW", "");
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static JSONObject saveSn(String oprno,String sn,int transferSn,int transferFloor,String user){
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesTransfer/saveSn";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn+"&transferSn="+transferSn+"&transferFloor="+transferFloor+"&ucode="+user;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                JdbcUtils.insertData(oprno, "100000", params, "MQDW", "");
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 上报CNC报警到服务端
+     * @param oprno    工位号,如 "OP040G"
+     * @param content  报警号(textIndex)
+     * @param type     报警类型(clearInfo)
+     * @param lineSn   产线编号
+     * @param sn       工件码(可为空)
+     * @return 服务端返回结果
+     */
+    public static JSONObject reportAlarm(String oprno, String content, String type, 
+                                          String lineSn, String sn) {
+        try {
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductRecord/pcdevice";
+            
+            // 构建参数:state=0 表示报警
+            StringBuilder params = new StringBuilder();
+            params.append("__ajax=json");
+            params.append("&state=0");
+            params.append("&oprno=").append(oprno != null ? oprno : "");
+            params.append("&content=").append(content != null ? content : "");
+            params.append("&type=").append(type != null ? type : "1");
+            params.append("&lineSn=").append(lineSn != null ? lineSn : "");
+            params.append("&sn=").append(sn != null ? sn : "");
+            
+            log.info("上报CNC报警: oprno={}, content={}, type={}, sn={}", oprno, content, type, sn);
+            String result = doPost(url, params.toString());
+            log.info("上报CNC报警结果: result=" + result);
+
+            if (result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            } else {
+                return JSONObject.parseObject(result);
+            }
+        } catch (Exception e) {
+            log.error("上报CNC报警异常: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 上传CNC参数到服务端
+     * @param upparams JSON格式的参数列表
+     * @return 服务端返回结果
+     */
+    public static JSONObject upCncParams(String upparams) {
+        try {
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductCncParam/batchsave";
+            String params = "__ajax=json&params=" + upparams;
+            log.info("上传CNC参数: params=" + params);
+            String result = doPost(url, params);
+            log.info("上传CNC参数结果: result=" + result);
+
+            if (result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            } else {
+                return JSONObject.parseObject(result);
+            }
+        } catch (Exception e) {
+            log.error("上传CNC参数异常: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 查询工作记录
+     * @param oprno 工位号(5位如OP040会查询所有子工位,空则查所有)
+     * @param sn 工件码(可选,用于搜索)
+     * @param pageNo 页码
+     * @param pageSize 每页条数
+     * @return 工作记录响应
+     */
+    public static WorkRecordResp getWorkRecordList(String oprno, String sn, int pageNo, int pageSize) {
+        WorkRecordResp resp = new WorkRecordResp();
+        try {
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductRecord/workData";
+            StringBuilder params = new StringBuilder();
+            params.append("__ajax=json");
+            params.append("&lineSn=").append(lineSn);
+            params.append("&pageNo=").append(pageNo);
+            params.append("&pageSize=").append(pageSize);
+            
+            if (oprno != null && !oprno.isEmpty()) {
+                params.append("&oprno=").append(oprno);
+            }
+            // 注意:不传auto参数,这样"所有工位"查询时会返回整条产线所有工位的数据
+            if (sn != null && !sn.isEmpty()) {
+                params.append("&sn=").append(sn);
+            }
+            
+            log.info("查询工作记录: url=" + url + ", params=" + params.toString());
+            String result = doPost(url, params.toString());
+            log.info("查询工作记录结果: result=" + result);
+            
+            if (result == null || result.trim().isEmpty()) {
+                resp.setResult(false);
+                resp.setMessage("请求返回空结果");
+                return resp;
+            }
+            
+            JSONObject jsonObj = JSONObject.parseObject(result);
+            if (jsonObj == null) {
+                resp.setResult(false);
+                resp.setMessage("解析响应失败");
+                return resp;
+            }
+            
+            resp.setResult(true);
+            resp.setPageNo(jsonObj.getIntValue("pageNo"));
+            resp.setPageSize(jsonObj.getIntValue("pageSize"));
+            resp.setCount(jsonObj.getLongValue("count"));
+            
+            List<WorkRecordData> list = new ArrayList<>();
+            JSONArray listArray = jsonObj.getJSONArray("list");
+            if (listArray != null) {
+                for (int i = 0; i < listArray.size(); i++) {
+                    JSONObject item = listArray.getJSONObject(i);
+                    WorkRecordData data = new WorkRecordData();
+                    data.setId(item.getString("id"));
+                    data.setSn(item.getString("sn"));
+                    data.setOprno(item.getString("oprno"));
+                    data.setUpdateBy(item.getString("updateBy"));
+                    data.setUpdateDate(item.getString("updateDate"));
+                    data.setContent(item.getString("content"));
+                    list.add(data);
+                }
+            }
+            resp.setList(list);
+            
+        } catch (Exception e) {
+            log.error("查询工作记录异常: " + e.getMessage());
+            e.printStackTrace();
+            resp.setResult(false);
+            resp.setMessage("查询异常: " + e.getMessage());
+        }
+        return resp;
+    }
+
+    public static String doPost(String httpUrl, String param) {
+        HttpURLConnection connection = null;
+        InputStream is = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        String result = null;
+        try {
+            URL url = new URL(httpUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setConnectTimeout(80000);
+            connection.setReadTimeout(8000);
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+            connection.setRequestProperty("Authorization", "Bearer da3efcbf-0845-4fe3-8aba-ee040be542c0");
+            os = connection.getOutputStream();
+            os.write(param.getBytes("UTF-8"));
+            if (connection.getResponseCode() == 200) {
+                is = connection.getInputStream();
+                br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+                StringBuffer sbf = new StringBuffer();
+                String temp = null;
+                while ((temp = br.readLine()) != null) {
+                    sbf.append(temp);
+                    sbf.append("\r\n");
+                }
+                result = sbf.toString();
+            }
+        } catch (MalformedURLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (null != br) {
+                try {
+                    br.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (null != os) {
+                try {
+                    os.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (null != is) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            connection.disconnect();
+        }
+        return result;
+    }
+}

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


Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov