hfy 14 годин тому
батько
коміт
3dac4039a1

+ 101 - 0
src/com/mes/device/DeviceStateReporter.java

@@ -0,0 +1,101 @@
+package com.mes.device;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.HttpUtils;
+import com.mes.util.StationConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 设备状态上报工具。
+ * 多工位客户端需要按每个工位分别提交,不能把 gw/lineSn 存成单例状态。
+ */
+public class DeviceStateReporter {
+    private static final Logger log = LoggerFactory.getLogger(DeviceStateReporter.class);
+
+    public static final String STATE_RUNNING = "1";
+    public static final String STATE_STOP = "7";
+    public static final String STATE_FAULT = "3";
+    public static final String STATE_baojing = STATE_FAULT;
+
+    private DeviceStateReporter() {
+    }
+
+    /**
+     * 后台上报多个工位状态,适合登录开机和运行心跳,避免阻塞 Swing 界面线程。
+     */
+    public static void reportStationsAsync(final List<StationConfig> stations, final String state, final String content) {
+        final List<StationConfig> stationSnapshot = snapshot(stations);
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                reportStationsSync(stationSnapshot, state, content);
+            }
+        }, "device-state-report-thread").start();
+    }
+
+    public static void reportAsync(final String serverIp, final String gw, final String lineSn,
+            final String state, final String content) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                reportSync(serverIp, gw, lineSn, state, content);
+            }
+        }, "device-state-report-thread-" + (gw == null ? "" : gw)).start();
+    }
+
+    /**
+     * 同步上报多个工位状态,退出/关闭程序时使用,尽量保证停机状态已发送。
+     */
+    public static int reportStationsSync(List<StationConfig> stations, String state, String content) {
+        int successCount = 0;
+        for (StationConfig station : snapshot(stations)) {
+            if (reportSync(station.getServerIp(), station.getGw(), station.getLineSn(), state, content)) {
+                successCount++;
+            }
+        }
+        return successCount;
+    }
+
+    /**
+     * 单工位状态上报接口,服务端按 gw + lineSn 识别设备。
+     */
+    public static boolean reportSync(String serverIp, String gw, String lineSn, String state, String content) {
+        if (isBlank(serverIp) || isBlank(gw) || isBlank(lineSn)) {
+            log.warn("skip device state report: serverIp={}, gw={}, lineSn={}", serverIp, gw, lineSn);
+            return false;
+        }
+        try {
+            String url = "http://" + serverIp + ":8980/js/a/mes/mesDevice/reportState";
+            JSONObject body = new JSONObject();
+            body.put("gw", gw);
+            body.put("lineSn", lineSn);
+            body.put("state", state);
+            body.put("content", content == null ? "" : content);
+            String result = HttpUtils.sendPostRequestJson(url, body.toJSONString());
+            log.info("device state report: gw={}, lineSn={}, state={}, result={}", gw, lineSn, state, result);
+            if (isBlank(result) || "false".equalsIgnoreCase(result)) {
+                return false;
+            }
+            JSONObject retObj = JSONObject.parseObject(result);
+            return retObj != null && "true".equalsIgnoreCase(String.valueOf(retObj.get("result")));
+        } catch (Exception e) {
+            log.error("device state report failed: gw={}, lineSn={}, state={}", gw, lineSn, state, e);
+            return false;
+        }
+    }
+
+    private static List<StationConfig> snapshot(List<StationConfig> stations) {
+        if (stations == null || stations.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return new ArrayList<>(stations);
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

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

@@ -175,6 +175,8 @@ public class LoginFarme extends JFrame {
                     //登录成功
                     MultiStationClient.welcomeWin.setVisible(false);
                     MultiStationClient.mainFrame.setUser(user_id);
+                    // 登录成功后上报当前工作站所有工位为开机/运行状态。
+                    MultiStationClient.mainFrame.reportDeviceRunning();
                     MultiStationClient.mainFrame.setVisible(true);
 //                    MesClient.mesClientFrame.setVisible(true);
 

+ 68 - 0
src/com/mes/ui/MultiStationClient.java

@@ -2,6 +2,7 @@ package com.mes.ui;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.mes.component.MesWebView;
+import com.mes.device.DeviceStateReporter;
 import com.mes.util.ConfigUtils;
 import com.mes.util.JdbcUtils;
 import com.mes.util.StationConfig;
@@ -16,12 +17,22 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
 
 import static com.mes.ui.WorkStationPanel.*;
 
 public class MultiStationClient extends JFrame {
     private static final Logger log = LoggerFactory.getLogger(MultiStationClient.class);
+    private static final int DEFAULT_DEVICE_HEARTBEAT_MINUTES = 5;
+    private static int deviceHeartbeatMinutes = DEFAULT_DEVICE_HEARTBEAT_MINUTES;
+
     private List<WorkStationPanel> stationPanels = new ArrayList<>();
+    // 当前工作站配置中的所有工位,用于开机、心跳、停机逐个上报。
+    private List<StationConfig> stationConfigs = new ArrayList<>();
+    private Timer deviceReportTimer;
+    // 避免窗口关闭和退出菜单连续触发时重复上报停机。
+    private boolean deviceStopReported = true;
 
     private JButton userMenu;
     private JLabel statusLabel;
@@ -49,6 +60,8 @@ public class MultiStationClient extends JFrame {
             try {
                 MesClient.readProperty();
                 Properties props = ConfigUtils.loadProperties();
+                // 设备运行心跳间隔可通过 mes.device.heartbeat.minutes 配置,未配置默认 5 分钟。
+                deviceHeartbeatMinutes = parseDeviceHeartbeatMinutes(props);
                 log.info("client config={}, stations={}, proxyEnabled={}, proxyBaseUrl={}",
                         ConfigUtils.getConfigPath(),
                         props.getProperty("mes.stations", ""),
@@ -83,6 +96,8 @@ public class MultiStationClient extends JFrame {
                 int choice = JOptionPane.showConfirmDialog(null, "确定要关闭窗口吗?", "关闭确认", 
                     JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
                 if (choice == JOptionPane.YES_OPTION) {
+                    // 关闭窗口视为设备停机,先同步上报所有工位再退出。
+                    reportDeviceStopped();
                     dispose();
                     System.exit(0);
                 }
@@ -106,6 +121,8 @@ public class MultiStationClient extends JFrame {
             int choice = JOptionPane.showConfirmDialog(null, "确定要退出吗?", "退出确认", 
                 JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
             if (choice == JOptionPane.YES_OPTION) {
+                // 菜单退出同样视为设备停机。
+                reportDeviceStopped();
                 System.exit(0);
             }
         });
@@ -156,6 +173,8 @@ public class MultiStationClient extends JFrame {
 
         try {
             List<StationConfig> stations = StationConfig.loadAllStations();
+            // 保存配置中的全部工位,状态上报时按列表逐个提交。
+            stationConfigs = new ArrayList<>(stations);
             if (stations.isEmpty()) {
                 JOptionPane.showMessageDialog(this, "未配置任何工位", "错误", JOptionPane.ERROR_MESSAGE);
                 return;
@@ -200,6 +219,55 @@ public class MultiStationClient extends JFrame {
         }
     }
 
+    public synchronized void reportDeviceRunning() {
+        deviceStopReported = false;
+        // 登录成功后上报开机/运行,当前工作站内每个工位都提交一次。
+        DeviceStateReporter.reportStationsAsync(stationConfigs, DeviceStateReporter.STATE_RUNNING, "工位登录,开始运行");
+        startDeviceReportTimer();
+        log.info("device running report submitted for {} stations", stationConfigs.size());
+    }
+
+    public synchronized void reportDeviceStopped() {
+        stopDeviceReportTimer();
+        if (deviceStopReported) {
+            return;
+        }
+        // 退出程序前同步上报停机,降低异步线程未执行完就退出的风险。
+        int successCount = DeviceStateReporter.reportStationsSync(stationConfigs, DeviceStateReporter.STATE_STOP, "工位退出,停止运行");
+        deviceStopReported = true;
+        log.info("device stop report submitted for {} stations, success={}", stationConfigs.size(), successCount);
+    }
+
+    private void startDeviceReportTimer() {
+        stopDeviceReportTimer();
+        long intervalMs = deviceHeartbeatMinutes * 60L * 1000L;
+        deviceReportTimer = new Timer("device-report-timer", true);
+        deviceReportTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                // 定时补报运行状态,保持 MES 侧设备在线状态。
+                DeviceStateReporter.reportStationsAsync(stationConfigs, DeviceStateReporter.STATE_RUNNING, "运行心跳");
+            }
+        }, intervalMs, intervalMs);
+        log.info("device heartbeat timer started, intervalMinutes={}", deviceHeartbeatMinutes);
+    }
+
+    private void stopDeviceReportTimer() {
+        if (deviceReportTimer != null) {
+            deviceReportTimer.cancel();
+            deviceReportTimer = null;
+        }
+    }
+
+    private static int parseDeviceHeartbeatMinutes(Properties props) {
+        try {
+            int minutes = Integer.parseInt(props.getProperty("mes.device.heartbeat.minutes", String.valueOf(DEFAULT_DEVICE_HEARTBEAT_MINUTES)).trim());
+            return minutes > 0 ? minutes : DEFAULT_DEVICE_HEARTBEAT_MINUTES;
+        } catch (Exception e) {
+            return DEFAULT_DEVICE_HEARTBEAT_MINUTES;
+        }
+    }
+
     private void addResetMenuItem(JMenu settingMenu, WorkStationPanel panel, StationConfig config) {
         JMenuItem resetMenu = new JMenuItem("刷新" + config.getGw() + "工件");
         resetMenu.setIcon(new ImageIcon(MesClient.class.getResource("/bg/reset_logo.png")));

+ 19 - 0
src/com/mes/ui/WorkStationPanel.java

@@ -2,6 +2,7 @@ package com.mes.ui;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.mes.component.MesWebView;
+import com.mes.device.DeviceStateReporter;
 import com.mes.util.PLCUtils;
 import com.mes.util.StationConfig;
 import org.slf4j.Logger;
@@ -67,6 +68,7 @@ public class WorkStationPanel extends JPanel {
     private int plcWorkflowFailCount = 0;
     private long lastPlcWorkflowWarnTime = 0L;
     private long nextPlcWorkflowTime = 0L;
+    private Boolean lastFaultSignal = null;
 
     private boolean iconREDFlag = true;
 
@@ -540,6 +542,7 @@ public class WorkStationPanel extends JPanel {
         }
         try {
             final PLCUtils.StationStatus status = PLCUtils.readStationStatus(stationConfig);
+            reportFaultStatusIfChanged(status.isFault());
             plcStatusReadFailCount = 0;
             nextPlcStatusReadTime = 0L;
             SwingUtilities.invokeLater(new Runnable() {
@@ -562,6 +565,22 @@ public class WorkStationPanel extends JPanel {
         }
     }
 
+    private void reportFaultStatusIfChanged(boolean fault) {
+        if (lastFaultSignal != null && lastFaultSignal.booleanValue() == fault) {
+            return;
+        }
+        Boolean oldFaultSignal = lastFaultSignal;
+        lastFaultSignal = Boolean.valueOf(fault);
+        if (oldFaultSignal == null && !fault) {
+            return;
+        }
+
+        String state = fault ? DeviceStateReporter.STATE_FAULT : DeviceStateReporter.STATE_RUNNING;
+        String content = fault ? "故障" : "故障回复";
+        DeviceStateReporter.reportAsync(serverIp, gw, lineSn, state, content);
+        log.info("PLC故障状态变化上报,工位={},fault={},state={},content={}", gw, fault, state, content);
+    }
+
     private void startGetCurSn(){
         if (getSnBeatTimer != null) {
             getSnBeatTimer.cancel();

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

@@ -8,7 +8,7 @@ plc.proxy.server.port=18080
 plc.proxy.connect_timeout_ms=2000
 plc.proxy.read_timeout_ms=5000
 
-mes.stations=OP050A,OP060A,OP070A,OP080A,OP090A,OP160A
+mes.stations=OP070A,OP080A,OP090A,OP160A
 
 # Include the shared station definitions from config.properties when exporting the jar,
 # or copy the same mes.station.* entries here for a standalone external config file.

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

@@ -1,6 +1,7 @@
 # Workstation 2 client configuration.
 mes.line_sn=HEVXT
 mes.server_ip=192.168.9.180
+#mes.server_ip=127.0.0.1
 
 plc.proxy.enabled=true
 plc.proxy.base_url=http://192.168.0.66:18080

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

@@ -11,7 +11,7 @@ plc.proxy.connect_timeout_ms=2000
 plc.proxy.read_timeout_ms=5000
 
 # \u5DE5\u4F4D\u5217\u8868\uFF08\u652F\u6301\u591A\u4E2A\u5DE5\u4F4D\uFF0C\u9017\u53F7\u5206\u9694\uFF09
-#mes.stations=OP050A,OP060A,OP070A,OP080A,OP090A,OP160A
+#mes.stations=OP070A,OP080A,OP090A,OP160A
 # \u5DE5\u4F5C\u7AD92\u5207\u6362\u5217\u8868\uFF08\u9700\u8981\u8FD0\u884C\u5DE5\u4F5C\u7AD92\u65F6\uFF0C\u5C06\u4E0A\u4E00\u884C mes.stations \u6CE8\u91CA\uFF0C\u53D6\u6D88\u4E0B\u4E00\u884C\u6CE8\u91CA\uFF09
 mes.stations=OP100A,OP110A,OP120A,OP130A,OP140A,OP150A