Преглед на файлове

MBDW,MQDW竞态,先MBDW后MQDW,拉铆过程参数

wangxichen преди 17 часа
родител
ревизия
611457b2c8

+ 62 - 0
src/com/mes/core/StationConfig.java

@@ -55,6 +55,8 @@ public class StationConfig {
     private HeartbeatConfig heartbeatConfig;
     // 继电器配置
     private RelayConfig relayConfig;
+    // 过程参数采集配置
+    private ProdParamsConfig prodParamsConfig;
 
     // ========== UI配置 ==========
     private boolean showDeviceInfo;
@@ -255,6 +257,21 @@ public class StationConfig {
                 relayConfig.setOpenCommand((String) relayObj.get("open_command"));
                 relayConfig.setCloseCommand((String) relayObj.get("close_command"));
             }
+            
+            // 解析过程参数采集配置
+            Map<String, Object> prodParamsObj = (Map<String, Object>) deviceObj.get("prod_params");
+            if (prodParamsObj != null) {
+                prodParamsConfig = new ProdParamsConfig();
+                prodParamsConfig.setEnabled(getBooleanValue(prodParamsObj, "enabled", false));
+                prodParamsConfig.setFoutAddress(getIntValue(prodParamsObj, "fout_address", 4112));
+                prodParamsConfig.setSoutAddress(getIntValue(prodParamsObj, "sout_address", 4120));
+                prodParamsConfig.setFminAddress(getIntValue(prodParamsObj, "fmin_address", 4116));
+                prodParamsConfig.setSminAddress(getIntValue(prodParamsObj, "smin_address", 4126));
+                prodParamsConfig.setFmaxAddress(getIntValue(prodParamsObj, "fmax_address", 4114));
+                prodParamsConfig.setSmaxAddress(getIntValue(prodParamsObj, "smax_address", 4124));
+                prodParamsConfig.setQtyAddress(getIntValue(prodParamsObj, "qty_address", 4));
+                prodParamsConfig.setUploadInterval(getIntValue(prodParamsObj, "upload_interval", 60));
+            }
         }
 
         // 解析UI配置
@@ -546,6 +563,17 @@ public class StationConfig {
         return relayConfig != null && relayConfig.isEnabled();
     }
 
+    public ProdParamsConfig getProdParamsConfig() {
+        return prodParamsConfig;
+    }
+
+    /**
+     * 检查是否启用过程参数采集
+     */
+    public boolean hasProdParams() {
+        return prodParamsConfig != null && prodParamsConfig.isEnabled();
+    }
+
     // ========== 内部类 ==========
 
     /**
@@ -783,4 +811,38 @@ public class StationConfig {
             return label != null && !label.trim().isEmpty();
         }
     }
+
+    /**
+     * 过程参数采集配置
+     */
+    public static class ProdParamsConfig {
+        private boolean enabled;
+        private int foutAddress = 4112;   // 力输出值 F-out
+        private int soutAddress = 4120;   // 行程输出值 S-out
+        private int fminAddress = 4116;   // 力最小值 F-min
+        private int sminAddress = 4126;   // 行程最小值 S-min
+        private int fmaxAddress = 4114;   // 力最大值 F-max
+        private int smaxAddress = 4124;   // 行程最大值 S-max
+        private int qtyAddress = 4;       // 数量寄存器地址
+        private int uploadInterval = 60;  // 上传间隔(秒)
+
+        public boolean isEnabled() { return enabled; }
+        public void setEnabled(boolean enabled) { this.enabled = enabled; }
+        public int getFoutAddress() { return foutAddress; }
+        public void setFoutAddress(int foutAddress) { this.foutAddress = foutAddress; }
+        public int getSoutAddress() { return soutAddress; }
+        public void setSoutAddress(int soutAddress) { this.soutAddress = soutAddress; }
+        public int getFminAddress() { return fminAddress; }
+        public void setFminAddress(int fminAddress) { this.fminAddress = fminAddress; }
+        public int getSminAddress() { return sminAddress; }
+        public void setSminAddress(int sminAddress) { this.sminAddress = sminAddress; }
+        public int getFmaxAddress() { return fmaxAddress; }
+        public void setFmaxAddress(int fmaxAddress) { this.fmaxAddress = fmaxAddress; }
+        public int getSmaxAddress() { return smaxAddress; }
+        public void setSmaxAddress(int smaxAddress) { this.smaxAddress = smaxAddress; }
+        public int getQtyAddress() { return qtyAddress; }
+        public void setQtyAddress(int qtyAddress) { this.qtyAddress = qtyAddress; }
+        public int getUploadInterval() { return uploadInterval; }
+        public void setUploadInterval(int uploadInterval) { this.uploadInterval = uploadInterval; }
+    }
 }

+ 39 - 0
src/com/mes/core/WorkflowEngine.java

@@ -166,6 +166,16 @@ public class WorkflowEngine {
         context.setCurrentStepIndex(0);  // 重置步骤索引到第一步
         running = true;
         paused = false;
+
+        // 重置所有步骤状态并重建消息映射(防止多阶段步骤的messageType残留)
+        messageStepMap.clear();
+        for (IWorkflowStep step : steps) {
+            step.reset();
+            String msgType = step.getMessageType();
+            if (msgType != null && !msgType.isEmpty()) {
+                messageStepMap.put(msgType, step);
+            }
+        }
     }
 
     /**
@@ -231,12 +241,26 @@ public class WorkflowEngine {
         }
 
         try {
+            // 记录执行前的messageType
+            String msgTypeBefore = step.getMessageType();
+
             // 执行步骤
             boolean result = step.execute(context);
 
             if (result) {
                 // 异步步骤:设置超时,等待服务端响应或轮询完成
                 if (step.isAsync()) {
+                    // 检查execute中是否切换了messageType(如MQDW->MBDW)
+                    String msgTypeAfter = step.getMessageType();
+                    if (msgTypeAfter != null && !msgTypeAfter.equals(msgTypeBefore)) {
+                        log.info("[{}] 步骤 {} execute后切换消息类型: {} -> {}", 
+                                context.getStationCode(), step.getStepId(), msgTypeBefore, msgTypeAfter);
+                        if (msgTypeBefore != null) {
+                            messageStepMap.remove(msgTypeBefore);
+                        }
+                        messageStepMap.put(msgTypeAfter, step);
+                    }
+
                     // 注册异步完成回调(用于轮询类步骤主动通知完成)
                     step.setAsyncCompleteCallback(success -> {
                         cancelTimeout();
@@ -292,9 +316,24 @@ public class WorkflowEngine {
             return;
         }
 
+        // 记录响应前的messageType
+        String oldMsgType = step.getMessageType();
+
         // 处理响应
         step.onServerResponse(context, result, message);
 
+        // 检查步骤是否切换了messageType(多阶段步骤,如先MBDW再MQDW)
+        String newMsgType = step.getMessageType();
+        if (newMsgType != null && !newMsgType.equals(oldMsgType)) {
+            log.info("[{}] 步骤 {} 切换消息类型: {} -> {}", context.getStationCode(), step.getStepId(), oldMsgType, newMsgType);
+            // 更新messageStepMap映射
+            messageStepMap.remove(oldMsgType);
+            messageStepMap.put(newMsgType, step);
+            // 重新启动超时,继续等待下一阶段的响应
+            startTimeout(step);
+            return;
+        }
+
         // 判断是否成功
         boolean success = "OK".equalsIgnoreCase(result) || "UD".equalsIgnoreCase(result);
         onStepComplete(step, success);

+ 9 - 0
src/com/mes/device/IDeviceDriver.java

@@ -149,4 +149,13 @@ public interface IDeviceDriver {
      */
     default void resetDevice() {
     }
+
+    /**
+     * 读取32位整数寄存器
+     * @param address 地址
+     * @return 值
+     */
+    default int readInt32(int address) {
+        return 0;
+    }
 }

+ 13 - 0
src/com/mes/device/ModbusTcpDriver.java

@@ -366,6 +366,19 @@ public class ModbusTcpDriver implements IDeviceDriver {
         }
     }
 
+    @Override
+    public int readInt32(int address) {
+        if (!isConnected()) {
+            return 0;
+        }
+        try {
+            return plc.readInt32(address);
+        } catch (Exception e) {
+            log.error("读取Int32寄存器失败, 地址{}: {}", address, e.getMessage());
+            return 0;
+        }
+    }
+
     // ========== Getters & Setters ==========
 
     public String getIp() {

+ 120 - 0
src/com/mes/prod/ProdDataCollector.java

@@ -0,0 +1,120 @@
+package com.mes.prod;
+
+import com.mes.core.StationConfig;
+import com.mes.core.StationContext;
+import com.mes.device.IDeviceDriver;
+import com.mes.util.DateLocalUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 拉铆过程参数采集器
+ * 在WaitCompleteStep轮询中,检测finishedCount变化时从PLC读取过程参数
+ * 采集的数据暂存在内存队列中,由ProdDataUploader定时上传
+ */
+public class ProdDataCollector {
+    private static final Logger log = LoggerFactory.getLogger(ProdDataCollector.class);
+
+    // 待上传的过程参数队列
+    private final List<ProdRecord> pendingRecords = new CopyOnWriteArrayList<>();
+
+    // 上一次的已打数量,用于检测变化
+    private volatile int lastFinishedCount = 0;
+
+    private final StationConfig.ProdParamsConfig paramsConfig;
+
+    public ProdDataCollector(StationConfig.ProdParamsConfig paramsConfig) {
+        this.paramsConfig = paramsConfig;
+    }
+
+    /**
+     * 在轮询中调用,检测finishedCount变化并采集过程参数
+     * @param context 工位上下文
+     * @param driver 设备驱动
+     * @param currentFinishedCount 当前已打数量
+     */
+    public void onPoll(StationContext context, IDeviceDriver driver, int currentFinishedCount) {
+        if (paramsConfig == null || !paramsConfig.isEnabled()) {
+            return;
+        }
+
+        // 检测已打数量是否增加
+        if (currentFinishedCount > lastFinishedCount) {
+            lastFinishedCount = currentFinishedCount;
+
+            String productSn = context.getProcessedProductSn();
+            if (productSn == null || productSn.trim().isEmpty()) {
+                log.warn("[{}] 工件码为空,跳过过程参数采集", context.getStationCode());
+                return;
+            }
+
+            try {
+                // 从PLC读取过程参数(Int32,除以1000得到实际值)
+                String fout = driver.readInt32(paramsConfig.getFoutAddress()) / 1000 + "";
+                String sout = (float) driver.readInt32(paramsConfig.getSoutAddress()) / 1000 + "";
+                String fmin = driver.readInt32(paramsConfig.getFminAddress()) / 1000 + "";
+                String smin = (float) driver.readInt32(paramsConfig.getSminAddress()) / 1000 + "";
+                String fmax = driver.readInt32(paramsConfig.getFmaxAddress()) / 1000 + "";
+                String smax = (float) driver.readInt32(paramsConfig.getSmaxAddress()) / 1000 + "";
+                String qty = String.valueOf(driver.readRegister(paramsConfig.getQtyAddress()));
+
+                // 构建记录
+                ProdRecord record = new ProdRecord();
+                record.setGw(context.getStationCode());
+                record.setLineSn(context.getLineSn());
+                record.setType("A");
+                record.setSn(productSn);
+                record.setFout(fout);
+                record.setSout(sout);
+                record.setFmin(fmin);
+                record.setSmin(smin);
+                record.setFmax(fmax);
+                record.setSmax(smax);
+                record.setQty(qty);
+                record.setSerialNumber(String.valueOf(currentFinishedCount));
+                record.setUcode(context.getUser());
+                record.setRecordTime(DateLocalUtils.getCurrentDateTime());
+
+                pendingRecords.add(record);
+                log.info("[{}] 采集过程参数: 第{}钉, fout={}, sout={}, fmin={}, smin={}, fmax={}, smax={}",
+                        context.getStationCode(), currentFinishedCount, fout, sout, fmin, smin, fmax, smax);
+
+            } catch (Exception e) {
+                log.error("[{}] 采集过程参数失败: {}", context.getStationCode(), e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 获取并清空待上传的记录
+     */
+    public List<ProdRecord> drainRecords() {
+        List<ProdRecord> records = new java.util.ArrayList<>(pendingRecords);
+        pendingRecords.removeAll(records);
+        return records;
+    }
+
+    /**
+     * 将上传失败的记录放回队列
+     */
+    public void requeue(List<ProdRecord> records) {
+        pendingRecords.addAll(0, records);
+    }
+
+    /**
+     * 获取待上传记录数量
+     */
+    public int getPendingCount() {
+        return pendingRecords.size();
+    }
+
+    /**
+     * 重置(新工件开始时调用)
+     */
+    public void reset() {
+        lastFinishedCount = 0;
+    }
+}

+ 119 - 0
src/com/mes/prod/ProdDataUploader.java

@@ -0,0 +1,119 @@
+package com.mes.prod;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.HttpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 拉铆过程参数上传器
+ * 定时将采集的过程参数通过HTTP批量上传到MES服务器
+ * 对应OP150的upParamsA逻辑
+ */
+public class ProdDataUploader {
+    private static final Logger log = LoggerFactory.getLogger(ProdDataUploader.class);
+
+    // 工位号 -> 采集器 的映射
+    private final Map<String, ProdDataCollector> collectors = new ConcurrentHashMap<>();
+    private Timer uploadTimer;
+    private final String serverIp;
+    private final int httpPort;
+    private final int uploadIntervalSec;
+
+    public ProdDataUploader(String serverIp, int httpPort, int uploadIntervalSec) {
+        this.serverIp = serverIp;
+        this.httpPort = httpPort;
+        this.uploadIntervalSec = uploadIntervalSec;
+    }
+
+    /**
+     * 注册采集器
+     */
+    public void addCollector(String stationCode, ProdDataCollector collector) {
+        collectors.put(stationCode, collector);
+    }
+
+    /**
+     * 启动定时上传
+     */
+    public void start() {
+        if (uploadTimer != null) {
+            uploadTimer.cancel();
+        }
+        uploadTimer = new Timer("ProdDataUpload", true);
+        uploadTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                uploadAll();
+            }
+        }, 5000, uploadIntervalSec * 1000L);
+        log.info("过程参数上传器已启动,间隔{}秒", uploadIntervalSec);
+    }
+
+    /**
+     * 停止上传
+     */
+    public void stop() {
+        if (uploadTimer != null) {
+            uploadTimer.cancel();
+            uploadTimer = null;
+        }
+        // 停止前尝试上传剩余数据
+        uploadAll();
+        log.info("过程参数上传器已停止");
+    }
+
+    /**
+     * 上传所有采集器的数据
+     */
+    private void uploadAll() {
+        for (Map.Entry<String, ProdDataCollector> entry : collectors.entrySet()) {
+            ProdDataCollector collector = entry.getValue();
+            List<ProdRecord> records = collector.drainRecords();
+            if (!records.isEmpty()) {
+                upload(collector, records);
+            }
+        }
+    }
+
+    /**
+     * 批量上传过程参数到MES服务器
+     */
+    private void upload(ProdDataCollector collector, List<ProdRecord> records) {
+        try {
+            String url = "http://" + serverIp + ":" + httpPort
+                    + "/js/a/mes/mesProductProd/batchsave";
+            String oprno = records.get(0).getGw();
+            String lineSn = records.get(0).getLineSn();
+            String jsonParams = JSON.toJSONString(records);
+            String params = "__ajax=json&oprno=" + oprno
+                    + "&lineSn=" + lineSn
+                    + "&params=" + jsonParams;
+
+            log.info("上传过程参数: 工位={}, 条数={}", oprno, records.size());
+
+            String result = HttpUtils.sendPost(url, params);
+
+            if (result != null && !result.equalsIgnoreCase("false")) {
+                JSONObject retObj = JSONObject.parseObject(result);
+                if (retObj != null && "true".equalsIgnoreCase(retObj.getString("result"))) {
+                    log.info("过程参数上传成功: {}条", records.size());
+                    return;
+                }
+            }
+            // 上传失败,放回队列等待下次重试
+            log.warn("过程参数上传失败,放回队列等待重试: {}", result);
+            collector.requeue(records);
+        } catch (Exception e) {
+            log.error("过程参数上传异常: {}", e.getMessage());
+            collector.requeue(records);
+        }
+    }
+}

+ 51 - 0
src/com/mes/prod/ProdRecord.java

@@ -0,0 +1,51 @@
+package com.mes.prod;
+
+/**
+ * 拉铆过程参数记录
+ * 对应OP150的bw_prod表结构
+ */
+public class ProdRecord {
+    private String gw;            // 工位号
+    private String lineSn;        // 线体号
+    private String type;          // 类型(A/B)
+    private String sn;            // 工件码
+    private String fout;          // 力输出值
+    private String sout;          // 行程输出值
+    private String fmin;          // 力最小值
+    private String smin;          // 行程最小值
+    private String fmax;          // 力最大值
+    private String smax;          // 行程最大值
+    private String qty;           // 数量
+    private String serialNumber;  // 序号(当前打钉数)
+    private String ucode;         // 操作员
+    private String recordTime;    // 记录时间
+
+    public String getGw() { return gw; }
+    public void setGw(String gw) { this.gw = gw; }
+    public String getLineSn() { return lineSn; }
+    public void setLineSn(String lineSn) { this.lineSn = lineSn; }
+    public String getType() { return type; }
+    public void setType(String type) { this.type = type; }
+    public String getSn() { return sn; }
+    public void setSn(String sn) { this.sn = sn; }
+    public String getFout() { return fout; }
+    public void setFout(String fout) { this.fout = fout; }
+    public String getSout() { return sout; }
+    public void setSout(String sout) { this.sout = sout; }
+    public String getFmin() { return fmin; }
+    public void setFmin(String fmin) { this.fmin = fmin; }
+    public String getSmin() { return smin; }
+    public void setSmin(String smin) { this.smin = smin; }
+    public String getFmax() { return fmax; }
+    public void setFmax(String fmax) { this.fmax = fmax; }
+    public String getSmax() { return smax; }
+    public void setSmax(String smax) { this.smax = smax; }
+    public String getQty() { return qty; }
+    public void setQty(String qty) { this.qty = qty; }
+    public String getSerialNumber() { return serialNumber; }
+    public void setSerialNumber(String serialNumber) { this.serialNumber = serialNumber; }
+    public String getUcode() { return ucode; }
+    public void setUcode(String ucode) { this.ucode = ucode; }
+    public String getRecordTime() { return recordTime; }
+    public void setRecordTime(String recordTime) { this.recordTime = recordTime; }
+}

+ 131 - 47
src/com/mes/step/UploadResultStep.java

@@ -5,8 +5,13 @@ import com.mes.core.StationContext;
 import com.mes.tcp.MessageSender;
 
 /**
- * 上传结果步骤
- * 发送MQDW消息上传加工结果
+ * 上传结果步骤(两阶段)
+ * 
+ * 如果配置了物料绑定,执行流程:
+ *   阶段1: 发送MBDW(物料绑定),等待服务端响应
+ *   阶段2: MBDW成功后,发送MQDW(质量结果),等待服务端响应
+ * 
+ * 如果没有物料,直接发送MQDW。
  * 
  * 支持两种提交模式:
  * - manual: 用户点击OK/NG后直接上传,不需要workStarted
@@ -14,9 +19,17 @@ import com.mes.tcp.MessageSender;
  */
 public class UploadResultStep extends AbstractStep {
 
+    /** 当前执行阶段 */
+    private enum Phase {
+        BIND_MATERIAL,  // 阶段1: 绑定物料(MBDW)
+        UPLOAD_RESULT   // 阶段2: 上传质量结果(MQDW)
+    }
+
     private MessageSender messageSender;
     private String result = "OK";  // 默认结果
     private boolean manualMode = true;  // 默认手动模式
+    private Phase currentPhase = Phase.UPLOAD_RESULT;  // 当前阶段
+    private boolean needBindMaterial = false;  // 是否需要绑定物料
 
     public UploadResultStep() {
         super("upload_result", "上传结果");
@@ -28,10 +41,8 @@ public class UploadResultStep extends AbstractStep {
         try {
             StationConfig config = StationConfig.getInstance();
             this.manualMode = config.isManualSubmitMode();
-            // 手动模式需要用户点击OK/NG,自动模式不需要用户交互
             this.requiresUserInteraction = this.manualMode;
         } catch (Exception e) {
-            // 配置未加载时使用默认值(手动模式)
             this.requiresUserInteraction = true;
         }
     }
@@ -66,40 +77,59 @@ public class UploadResultStep extends AbstractStep {
         context.setSubmitting(true);
         context.setState(StationContext.WorkflowState.UPLOADING);
 
-        // 步骤1: 如果有物料且未绑定,先绑定物料
+        // 判断是否需要先绑定物料
         StationConfig config = StationConfig.getInstance();
-        if (config.isShowMaterialInput() && context.getMaterialSn() != null && !context.getMaterialSn().isEmpty()) {
-            if (!context.isMaterialBound()) {
-                log.info("[{}] 开始绑定物料: {}", context.getStationCode(), context.getMaterialSn());
-                context.setStatusMessage("正在绑定物料...", 0);
-                
-                if (messageSender != null) {
-                    boolean bindResult = messageSender.sendBindMaterial(
-                            productSn,
-                            context.getMaterialSn(),
-                            context.getUser(),
-                            "400004",  // 物料绑定工艺号
-                            context.getStationCode()
-                    );
-                    
-                    if (!bindResult) {
-                        log.error("[{}] 物料绑定失败", context.getStationCode());
-                        context.setStatusMessage("物料绑定失败,自动提交中止", -1);
-                        context.setSubmitting(false);
-                        return false;
-                    }
-                    
-                    context.setMaterialBound(true);
-                    log.info("[{}] 物料绑定成功", context.getStationCode());
-                } else {
-                    log.error("[{}] MessageSender未设置", context.getStationCode());
-                    context.setSubmitting(false);
-                    return false;
-                }
+        needBindMaterial = config.isShowMaterialInput()
+                && context.getMaterialSn() != null
+                && !context.getMaterialSn().isEmpty()
+                && !context.isMaterialBound();
+
+        if (needBindMaterial) {
+            // 阶段1: 先发MBDW绑定物料,等待响应
+            currentPhase = Phase.BIND_MATERIAL;
+            this.messageType = "MBDW";  // 切换messageType,让引擎路由MBDW响应到本步骤
+            
+            log.info("[{}] 开始绑定物料: {}", context.getStationCode(), context.getMaterialSn());
+            context.setStatusMessage("正在绑定物料...", 0);
+            
+            if (messageSender == null) {
+                log.error("[{}] MessageSender未设置", context.getStationCode());
+                context.setSubmitting(false);
+                return false;
+            }
+            
+            boolean sendOk = messageSender.sendBindMaterial(
+                    productSn,
+                    context.getMaterialSn(),
+                    context.getUser(),
+                    "400004",  // 物料绑定工艺号
+                    context.getStationCode()
+            );
+            
+            if (!sendOk) {
+                log.error("[{}] 物料绑定消息发送失败", context.getStationCode());
+                context.setStatusMessage("物料绑定失败,自动提交中止", -1);
+                context.setSubmitting(false);
+                this.messageType = "MQDW";  // 恢复
+                currentPhase = Phase.UPLOAD_RESULT;
+                return false;
             }
+            
+            // 返回true,等待MBDW服务端响应(引擎会设置超时)
+            return true;
+        } else {
+            // 不需要绑定物料,直接发MQDW
+            currentPhase = Phase.UPLOAD_RESULT;
+            this.messageType = "MQDW";
+            return sendQualityResult(context);
         }
+    }
 
-        // 步骤2: 提交质量结果
+    /**
+     * 发送质量结果(MQDW)
+     */
+    private boolean sendQualityResult(StationContext context) {
+        String productSn = context.getProcessedProductSn();
         context.setStatusMessage("正在提交结果...", 0);
         log.info("[{}] 上传结果: sn={}, result={}, craft={}", context.getStationCode(), productSn, result, craft);
 
@@ -120,19 +150,64 @@ public class UploadResultStep extends AbstractStep {
 
     @Override
     public void onServerResponse(StationContext context, String resultCode, String message) {
+        if (currentPhase == Phase.BIND_MATERIAL) {
+            // 阶段1响应: MBDW物料绑定结果
+            handleBindMaterialResponse(context, resultCode, message);
+        } else {
+            // 阶段2响应: MQDW质量结果上传
+            handleUploadResultResponse(context, resultCode, message);
+        }
+    }
+
+    /**
+     * 处理MBDW物料绑定响应
+     */
+    private void handleBindMaterialResponse(StationContext context, String resultCode, String message) {
+        if ("OK".equalsIgnoreCase(resultCode)) {
+            // 绑定成功,切换到阶段2
+            context.setMaterialBound(true);
+            log.info("[{}] 物料绑定成功,继续上传质量结果", context.getStationCode());
+            
+            // 先切换messageType(让引擎检测到变化,不走onStepComplete)
+            currentPhase = Phase.UPLOAD_RESULT;
+            this.messageType = "MQDW";
+            
+            // 发送MQDW
+            boolean sendOk = sendQualityResult(context);
+            if (!sendOk) {
+                // 发送失败,通过回调通知引擎步骤失败
+                log.error("[{}] 质量结果消息发送失败", context.getStationCode());
+                context.setSubmitting(false);
+                context.setStatusMessage("结果提交失败", -1);
+                notifyAsyncComplete(false);
+            }
+            // 发送成功则等待MQDW响应(引擎检测到messageType变化会更新映射并继续等待)
+        } else {
+            // 绑定失败,messageType保持MBDW不变,引擎会调用onStepComplete(false)
+            context.setSubmitting(false);
+            String errorMsg = "物料绑定失败: " + resultCode;
+            context.setStatusMessage(errorMsg, -1);
+            log.error("[{}] {}", context.getStationCode(), errorMsg);
+        }
+    }
+
+    /**
+     * 处理MQDW质量结果上传响应
+     */
+    private void handleUploadResultResponse(StationContext context, String resultCode, String message) {
         context.setSubmitting(false);
 
         if ("OK".equalsIgnoreCase(resultCode)) {
             context.setResultUploaded(true);
             log.info("[{}] 结果上传成功", context.getStationCode());
             
-            // 步骤3: 清零PLC计数器(地址66、68、70)
+            // 清零PLC计数器(地址66、68、70)
             if (context.getDeviceDriver() != null) {
                 try {
                     log.info("[{}] 开始清零PLC计数器", context.getStationCode());
-                    context.getDeviceDriver().writeRegister(66, 0);  // 完成数量
-                    context.getDeviceDriver().writeRegister(68, 0);  // 合格数量
-                    context.getDeviceDriver().writeRegister(70, 0);  // 不合格数量
+                    context.getDeviceDriver().writeRegister(66, 0);
+                    context.getDeviceDriver().writeRegister(68, 0);
+                    context.getDeviceDriver().writeRegister(70, 0);
                     log.info("[{}] PLC计数器清零成功", context.getStationCode());
                 } catch (Exception e) {
                     log.error("[{}] PLC计数器清零失败: {}", context.getStationCode(), e.getMessage());
@@ -153,12 +228,27 @@ public class UploadResultStep extends AbstractStep {
     @Override
     public void onTimeout(StationContext context) {
         context.setSubmitting(false);
+        // 超时时恢复到初始状态
+        resetPhase();
         super.onTimeout(context);
     }
 
+    /**
+     * 重置阶段状态(流程重置或超时时调用)
+     */
+    private void resetPhase() {
+        currentPhase = Phase.UPLOAD_RESULT;
+        this.messageType = "MQDW";
+        needBindMaterial = false;
+    }
+
+    @Override
+    public void reset() {
+        resetPhase();
+    }
+
     public void setManualMode(boolean manualMode) {
         this.manualMode = manualMode;
-        // 手动模式需要用户交互,自动模式不需要
         this.requiresUserInteraction = manualMode;
     }
 
@@ -177,32 +267,25 @@ public class UploadResultStep extends AbstractStep {
         // 如果配置了物料,需要检查物料校验状态
         StationConfig config = StationConfig.getInstance();
         if (config.isShowMaterialInput()) {
-            // 有物料配置:需要物料已校验
             boolean materialCondition = context.isMaterialValidated();
             log.debug("[{}] UploadResultStep.canExecute (with material): qualityPassed={}, materialValidated={}, resultUploaded={}", 
                     context.getStationCode(), context.isQualityPassed(), context.isMaterialValidated(), context.isResultUploaded());
             
-            // 判断是否需要等待设备完成
             if (context.isDeviceEnabled()) {
-                // 有设备:需要等待设备完成
                 boolean result = basicCondition && materialCondition && context.isWorkStarted();
                 log.debug("[{}] UploadResultStep.canExecute (device enabled): workStarted={}, result={}", 
                         context.getStationCode(), context.isWorkStarted(), result);
                 return result;
             } else {
-                // 无设备:质量检查通过 + 物料已校验即可
                 return basicCondition && materialCondition;
             }
         } else {
-            // 无物料配置:按原逻辑
             if (context.isDeviceEnabled()) {
-                // 有设备:需要等待设备完成
                 boolean result = basicCondition && context.isWorkStarted();
                 log.debug("[{}] UploadResultStep.canExecute (device enabled, no material): qualityPassed={}, workStarted={}, resultUploaded={}, result={}", 
                         context.getStationCode(), context.isQualityPassed(), context.isWorkStarted(), context.isResultUploaded(), result);
                 return result;
             } else {
-                // 无设备:质量检查通过即可
                 log.debug("[{}] UploadResultStep.canExecute (device disabled, no material): qualityPassed={}, resultUploaded={}, result={}", 
                         context.getStationCode(), context.isQualityPassed(), context.isResultUploaded(), basicCondition);
                 return basicCondition;
@@ -243,6 +326,7 @@ public class UploadResultStep extends AbstractStep {
         context.setSubmitting(false);
         context.setResultUploaded(true);
         context.setStatusMessage("(模拟) 结果提交成功,请扫下一件", 0);
+        resetPhase();
         onSuccess(context);
     }
-}
+}

+ 17 - 0
src/com/mes/step/WaitCompleteStep.java

@@ -3,6 +3,7 @@ package com.mes.step;
 import com.mes.core.StationConfig;
 import com.mes.core.StationContext;
 import com.mes.device.IDeviceDriver;
+import com.mes.prod.ProdDataCollector;
 
 import java.util.concurrent.*;
 
@@ -29,6 +30,9 @@ public class WaitCompleteStep extends AbstractStep {
     
     // 完成回调
     private CompletionCallback callback;
+    
+    // 过程参数采集器
+    private ProdDataCollector prodDataCollector;
 
     public interface CompletionCallback {
         void onComplete(StationContext context, boolean success, String result);
@@ -130,6 +134,11 @@ public class WaitCompleteStep extends AbstractStep {
                     context.setFinishedCount(finishedCount);
                     context.setPresetCount(presetCount);
 
+                    // 采集过程参数(检测finishedCount变化时从PLC读取)
+                    if (prodDataCollector != null) {
+                        prodDataCollector.onPoll(context, driver, finishedCount);
+                    }
+
                     // 读取第二行设备数据(如果配置了)
                     StationConfig config = StationConfig.getInstance();
                     StationConfig.DeviceInfoRow row2 = config.getDeviceInfoRow(1);
@@ -312,9 +321,17 @@ public class WaitCompleteStep extends AbstractStep {
         return waiting;
     }
 
+    public void setProdDataCollector(ProdDataCollector prodDataCollector) {
+        this.prodDataCollector = prodDataCollector;
+    }
+
     @Override
     public void reset() {
         stopPolling();
         waiting = false;
+        // 重置采集器(新工件开始时)
+        if (prodDataCollector != null) {
+            prodDataCollector.reset();
+        }
     }
 }

+ 36 - 0
src/com/mes/ui/MainFrame.java

@@ -3,9 +3,12 @@ package com.mes.ui;
 import com.mes.core.*;
 import com.mes.device.DeviceDriverFactory;
 import com.mes.device.IDeviceDriver;
+import com.mes.prod.ProdDataCollector;
+import com.mes.prod.ProdDataUploader;
 import com.mes.step.StartWorkStep;
 import com.mes.step.StepFactory;
 import com.mes.step.UploadResultStep;
+import com.mes.step.WaitCompleteStep;
 import com.mes.tcp.MesTcpClient;
 import com.mes.tcp.MessageDispatcher;
 import com.mes.ui.component.WorkstationPanel;
@@ -43,6 +46,9 @@ public class MainFrame extends JFrame {
     // 步骤工厂
     private StepFactory stepFactory;
 
+    // 过程参数上传器
+    private ProdDataUploader prodDataUploader;
+
     // 当前用户和会话信息
     private String currentUser = "";
     private String sessionId;
@@ -432,6 +438,13 @@ public class MainFrame extends JFrame {
             deviceDrivers = DeviceDriverFactory.createDrivers(config);
         }
 
+        // 初始化过程参数上传器
+        if (config.hasProdParams()) {
+            int uploadInterval = config.getProdParamsConfig().getUploadInterval();
+            prodDataUploader = new ProdDataUploader(
+                    config.getServerIp(), config.getHttpPort(), uploadInterval);
+        }
+
         for (int i = 0; i < config.getStationCount(); i++) {
             StationConfig.StationInfo stationInfo = config.getStation(i);
             if (stationInfo == null) continue;
@@ -455,6 +468,19 @@ public class MainFrame extends JFrame {
             // 创建流程引擎
             WorkflowEngine engine = new WorkflowEngine(context);
             engine.buildFromConfig(config.getWorkflowSteps(), stepFactory);
+            
+            // 注入过程参数采集器到WaitCompleteStep
+            if (config.hasProdParams()) {
+                ProdDataCollector collector = new ProdDataCollector(config.getProdParamsConfig());
+                for (com.mes.step.IWorkflowStep step : engine.getSteps()) {
+                    if (step instanceof WaitCompleteStep) {
+                        ((WaitCompleteStep) step).setProdDataCollector(collector);
+                        log.info("[{}] 过程参数采集器已注入", stationInfo.getCode());
+                    }
+                }
+                prodDataUploader.addCollector(stationInfo.getCode(), collector);
+            }
+            
             engine.setListener(new WorkflowEngine.WorkflowListener() {
                 @Override
                 public void onStepStarted(WorkflowEngine eng, com.mes.step.IWorkflowStep step) {
@@ -505,6 +531,11 @@ public class MainFrame extends JFrame {
             workflowMonitorPanel.setWorkstations(engines, contexts);
             log.info("流程监控面板初始化完成");
         }
+
+        // 启动过程参数上传器
+        if (prodDataUploader != null) {
+            prodDataUploader.start();
+        }
     }
 
     /**
@@ -717,6 +748,11 @@ public class MainFrame extends JFrame {
         log.info("正在关闭...");
         stopHeartBeatDisplay();
 
+        // 停止过程参数上传器(会尝试上传剩余数据)
+        if (prodDataUploader != null) {
+            prodDataUploader.stop();
+        }
+
         // 停止流程监控面板的刷新
         if (workflowMonitorPanel != null) {
             workflowMonitorPanel.stopRefresh();