统一卧转立扫码、卧转立、大车、大理片笼的定时器逻辑和步骤状态;添加设备拓扑图清除数据、联机状态切换按钮,
15个文件已修改
1355 ■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java 206 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 456 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue 203 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 84 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java
@@ -61,6 +61,10 @@
    @TableField("description")
    private String description;
    @ApiModelProperty(value = "产线编号")
    @TableField("work_line")
    private Integer workLine;
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -1,10 +1,12 @@
package com.mes.interaction.vehicle.handler;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceStatus;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceGroupRelationService;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.service.DeviceStatusService;
import com.mes.device.vo.DeviceGroupVO;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.BaseDeviceLogicHandler;
@@ -49,6 +51,9 @@
    
    @Autowired(required = false)
    private DeviceGroupRelationService deviceGroupRelationService;
    @Autowired(required = false)
    private DeviceStatusService deviceStatusService;
    
    @Autowired(required = false)
    private PlcDynamicDataService plcDynamicDataService;
@@ -189,6 +194,9 @@
                case "stopTaskMonitor":
                    result = handleStopTaskMonitor(deviceConfig);
                    break;
                case "setOnlineState":
                    result = handleSetOnlineState(deviceConfig, params, logicParams);
                    break;
                default:
                    log.warn("不支持的操作类型: {}", operation);
                    result = DevicePlcVO.OperationResult.builder()
@@ -221,8 +229,14 @@
     * 判断操作是否需要状态检查
     */
    private boolean needsStateCheck(String operation) {
        // 所有操作都需要检查状态,除了查询类操作
        return !"query".equals(operation) && !"status".equals(operation);
        // 所有操作都需要检查状态,除了查询类操作和特定内部检查
        if ("query".equals(operation) || "status".equals(operation)) {
            return false;
        }
        if ("checkMesConfirm".equals(operation)) {
            return false;
        }
        return true;
    }
    /**
@@ -302,23 +316,7 @@
        // 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取)
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
        // 优先使用运行时参数中的glassIntervalMs(从任务参数传入),如果没有则使用设备配置的
        Integer glassIntervalMs = null;
        if (params.containsKey("glassIntervalMs") && params.get("glassIntervalMs") != null) {
            Object intervalObj = params.get("glassIntervalMs");
            if (intervalObj instanceof Number) {
                glassIntervalMs = ((Number) intervalObj).intValue();
            } else if (intervalObj instanceof String) {
                try {
                    glassIntervalMs = Integer.parseInt((String) intervalObj);
                } catch (NumberFormatException e) {
                    // 忽略
                }
            }
        }
        if (glassIntervalMs == null) {
            glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
        }
        Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
        Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
        Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
@@ -335,7 +333,7 @@
        Integer positionValue = (Integer) params.get("positionValue");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
        List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
        List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity, glassGap,
                deviceConfig.getDeviceId());
        if (plannedGlasses == null) {
            // 玻璃没有长度时返回null表示错误
@@ -371,17 +369,21 @@
        }
        payload.put("plcGlassCount", plcSlots);
        // 写入位置信息
        // 写入位置信息:PLC侧期望的是 MES 编号(如1001/1002),而不是位置映射后的格子值
        Integer plcPosition = null;
        if (positionValue != null) {
            payload.put("inPosition", positionValue);
            // 如果调用方直接传了数值,则认为这是MES编号,直接写入
            plcPosition = positionValue;
        } else if (positionCode != null) {
            // 从位置映射中获取位置值
            @SuppressWarnings("unchecked")
            Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
            Integer mappedValue = positionMapping.get(positionCode);
            if (mappedValue != null) {
                payload.put("inPosition", mappedValue);
            // 尝试将位置代码解析为数字(例如 "900" -> 900)
            try {
                plcPosition = Integer.parseInt(positionCode.trim());
            } catch (NumberFormatException ignore) {
                // 非数字编码时,不写入inPosition,由PLC或后续逻辑自行处理
            }
        }
        if (plcPosition != null) {
            payload.put("inPosition", plcPosition);
        }
        // 自动触发请求字
@@ -401,11 +403,7 @@
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
            deviceConfig.getId(), payload, operationName);
        
        // 注意:glassIntervalMs 的等待应该在批次之间(在TaskExecutionEngine中处理),
        // 而不是在这里等待,因为这里等待会阻塞大车的正常装玻璃流程
        // 如果需要在写入后等待,应该在批次之间等待,让大车有时间处理当前批次的玻璃
        // 如果执行成功,更新位置信息到状态,并启动状态监控
        // 如果执行成功,更新位置信息到状态
        if (Boolean.TRUE.equals(result.getSuccess())) {
            VehicleStatus status = statusManager.getOrCreateVehicleStatus(
                deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
@@ -413,9 +411,16 @@
                VehiclePosition position = new VehiclePosition(positionCode, positionValue);
                status.setCurrentPosition(position);
            }
            // 启动自动状态监控,当 state=1 时自动协调卧转立设备
            startStateMonitoring(deviceConfig, logicParams);
            // 仅在“非多设备任务”场景下,才启动大车自身的自动状态监控和 MES 任务监控
            boolean inMultiDeviceTask = params != null && params.containsKey("_taskContext");
            if (!inMultiDeviceTask) {
                // 启动自动状态监控,当 state=1 时自动协调卧转立设备
                startStateMonitoring(deviceConfig, logicParams);
                // 从 PLC/MES 创建正式任务并启动监控的逻辑,保留给独立 MES 场景使用
                // 多设备任务场景下,这部分交由 TaskExecutionEngine 统一编排
            }
        }
        
        return result;
@@ -470,6 +475,7 @@
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        payload.put("onlineState", Boolean.TRUE);
        
        log.info("大车设备重置: deviceId={}", deviceConfig.getId());
        
@@ -484,8 +490,65 @@
            statusManager.clearVehicleTask(deviceConfig.getDeviceId());
            statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
            stopStateMonitoring(deviceConfig.getDeviceId());
            updateDeviceOnlineStatus(deviceConfig, true);
        }
        
        return result;
    }
    /**
     * 设置联机状态
     * @param deviceConfig 设备配置
     * @param params 参数,可包含 onlineState(1=联机,0=脱机)
     * @param logicParams 逻辑参数
     * @return 操作结果
     */
    private DevicePlcVO.OperationResult handleSetOnlineState(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // 从参数中获取联机状态值,默认为true(联机)
        boolean onlineState = true;
        if (params != null && params.containsKey("onlineState")) {
            Object stateObj = params.get("onlineState");
            if (stateObj instanceof Boolean) {
                onlineState = (Boolean) stateObj;
            } else if (stateObj instanceof Number) {
                onlineState = ((Number) stateObj).intValue() != 0;
            } else if (stateObj instanceof String) {
                try {
                    String str = ((String) stateObj).trim();
                    if ("true".equalsIgnoreCase(str)) {
                        onlineState = true;
                    } else if ("false".equalsIgnoreCase(str)) {
                        onlineState = false;
                    } else {
                        onlineState = Integer.parseInt(str) != 0;
                    }
                } catch (NumberFormatException e) {
                    log.warn("解析onlineState失败,使用默认值true: deviceId={}, value={}",
                            deviceConfig.getId(), stateObj);
                }
            }
        }
        Map<String, Object> payload = new HashMap<>();
        payload.put("onlineState", onlineState);
        String stateText = onlineState ? "联机" : "脱机";
        log.info("大车设备设置联机状态: deviceId={}, onlineState={} ({})",
                deviceConfig.getId(), onlineState, stateText);
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大车设备-设置联机状态(" + stateText + ")"
        );
        if (Boolean.TRUE.equals(result.getSuccess())) {
            updateDeviceOnlineStatus(deviceConfig, onlineState);
        }
        return result;
    }
@@ -512,6 +575,7 @@
        payload.put("plcGlassCount", 0);
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        payload.put("onlineState", Boolean.TRUE);
        if (params != null && params.containsKey("positionValue")) {
            payload.put("inPosition", params.get("positionValue"));
@@ -532,9 +596,23 @@
            statusManager.clearVehicleTask(deviceConfig.getDeviceId());
            statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
            stopStateMonitoring(deviceConfig.getDeviceId());
            updateDeviceOnlineStatus(deviceConfig, true);
        }
        
        return result;
    }
    private void updateDeviceOnlineStatus(DeviceConfig deviceConfig, boolean online) {
        if (deviceStatusService == null || deviceConfig == null || deviceConfig.getId() == null) {
            return;
        }
        try {
            String status = online ? DeviceStatus.Status.ONLINE : DeviceStatus.Status.OFFLINE;
            deviceStatusService.updateDeviceOnlineStatus(deviceConfig.getId(), status);
        } catch (Exception e) {
            log.warn("同步设备在线状态到数据库失败: deviceId={}, online={}, error={}",
                    deviceConfig.getDeviceId(), online, e.getMessage());
        }
    }
    private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) {
@@ -572,9 +650,9 @@
            return "车辆容量(vehicleCapacity)必须大于0";
        }
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null);
        if (glassIntervalMs != null && glassIntervalMs < 0) {
            return "玻璃间隔时间(glassIntervalMs)不能为负数";
        Integer glassGap = getLogicParam(logicParams, "glassGap", null);
        if (glassGap != null && glassGap < 0) {
            return "玻璃间隔(glassGap)不能为负数";
        }
        return null; // 验证通过
@@ -584,7 +662,7 @@
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        defaultParams.put("vehicleCapacity", 6000);
        defaultParams.put("glassIntervalMs", 1000);
        defaultParams.put("glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
        defaultParams.put("autoFeed", true);
        defaultParams.put("maxRetryCount", 5);
        defaultParams.put("defaultGlassLength", 2000);
@@ -687,16 +765,19 @@
    /**
     * 规划玻璃装载
     * @param source 源玻璃列表
     * @param vehicleCapacity 车辆容量
     * @param vehicleCapacity 车辆容量(mm)
     * @param glassGap 玻璃之间的物理间隔(mm),默认200mm
     * @param deviceId 设备ID(用于日志)
     * @return 规划后的玻璃列表,如果玻璃没有长度则返回null(用于测试MES程序)
     */
    private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
                                             int vehicleCapacity,
                                             int glassGap,
                                             String deviceId) {
        List<GlassInfo> planned = new ArrayList<>();
        int usedLength = 0;
        int capacity = Math.max(vehicleCapacity, 1);
        int gap = Math.max(glassGap, 0); // 确保间隔不为负数
        for (GlassInfo info : source) {
            Integer glassLength = info.getLength();
@@ -711,17 +792,26 @@
            int length = glassLength;
            
            if (planned.isEmpty()) {
                // 第一块玻璃,不需要间隙
                planned.add(info.withLength(length));
                usedLength = length;
                continue;
            }
            if (usedLength + length <= capacity) {
            // 后续玻璃需要考虑间隙:玻璃长度 + 间隙
            int requiredLength = length + gap;
            if (usedLength + requiredLength <= capacity) {
                planned.add(info.withLength(length));
                usedLength += length;
                usedLength += requiredLength; // 包含间隙
            } else {
                // 装不下了,停止添加
                break;
            }
        }
        log.debug("玻璃装载规划: deviceId={}, total={}, planned={}, usedLength={}, capacity={}, glassGap={}",
                deviceId, source.size(), planned.size(), usedLength, capacity, gap);
        return planned;
    }
@@ -1626,65 +1716,80 @@
            log.info("已给MES汇报({}任务): deviceId={}, glassId={}", 
                    taskType, deviceConfig.getDeviceId(), taskInfo.glassId);
            
            // 等待MES确认
            waitForMesConfirm(deviceConfig, serializer, taskInfo, logicParams);
            // 多设备任务场景下,不在这里阻塞等待MES确认,由任务引擎定时调用checkMesConfirm
        } catch (Exception e) {
            log.error("给MES汇报异常: deviceId={}", deviceConfig.getDeviceId(), e);
        }
    }
    /**
     * 等待MES确认
     * 检查MES确认状态(供任务引擎周期性调用)
     * 返回OperationResult.data中的 completed 标志表示是否已确认完成
     */
    private void waitForMesConfirm(DeviceConfig deviceConfig,
                                  EnhancedS7Serializer serializer,
                                  MesTaskInfo taskInfo,
                                  Map<String, Object> logicParams) {
    public DevicePlcVO.OperationResult checkMesConfirm(DeviceConfig deviceConfig,
                                                       Map<String, Object> logicParams) {
        if (plcDynamicDataService == null || s7SerializerProvider == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("PlcDynamicDataService或S7SerializerProvider未注入")
                    .build();
        }
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("获取PLC序列化器失败")
                    .build();
        }
        Map<String, Object> data = new HashMap<>();
        try {
            // 读取确认字(假设字段名为mesConfirm)
            Integer maxWaitTime = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000); // 默认30秒
            long startTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - startTime < maxWaitTime) {
                Object confirmValue = plcDynamicDataService.readPlcField(
                        deviceConfig, "mesConfirm", serializer);
                Integer confirm = parseInteger(confirmValue);
                if (confirm != null && confirm == 1) {
                    // MES已确认,清空state和汇报字
                    clearTaskStates(deviceConfig, serializer);
                    // 任务完成,恢复为空闲状态
                    statusManager.updateVehicleStatus(
                            deviceConfig.getDeviceId(), VehicleState.IDLE);
                    statusManager.clearVehicleTask(deviceConfig.getDeviceId());
                    // 移除任务
                    currentTasks.remove(deviceConfig.getDeviceId());
                    // 停止任务监控
                    handleStopTaskMonitor(deviceConfig);
                    // 恢复plcRequest=1(可以接收新任务)
                    Map<String, Object> payload = new HashMap<>();
                    payload.put("plcRequest", 1);
                    plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
                    log.info("MES任务已完成: deviceId={}, glassId={}",
                            deviceConfig.getDeviceId(), taskInfo.glassId);
                    return;
                }
                Thread.sleep(500); // 等待500ms后重试
            Object confirmValue = plcDynamicDataService.readPlcField(
                    deviceConfig, "mesConfirm", serializer);
            Integer confirm = parseInteger(confirmValue);
            boolean completed = confirm != null && confirm == 1;
            data.put("completed", completed);
            if (completed) {
                // MES已确认,清空state和汇报字
                clearTaskStates(deviceConfig, serializer);
                // 任务完成,恢复为空闲状态
                statusManager.updateVehicleStatus(
                        deviceConfig.getDeviceId(), VehicleState.IDLE);
                statusManager.clearVehicleTask(deviceConfig.getDeviceId());
                // 移除任务记录(如果有)
                currentTasks.remove(deviceConfig.getDeviceId());
                // 停止任务监控
                handleStopTaskMonitor(deviceConfig);
                // 恢复plcRequest=1(可以接收新任务)
                Map<String, Object> payload = new HashMap<>();
                payload.put("plcRequest", 1);
                plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
                log.info("MES任务已确认完成: deviceId={}", deviceConfig.getDeviceId());
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("MES任务已确认完成")
                        .data(data)
                        .build();
            }
            log.warn("等待MES确认超时: deviceId={}, glassId={}",
                    deviceConfig.getDeviceId(), taskInfo.glassId);
            return DevicePlcVO.OperationResult.builder()
                    .success(true)
                    .message("等待MES确认中")
                    .data(data)
                    .build();
        } catch (Exception e) {
            log.error("等待MES确认异常: deviceId={}", deviceConfig.getDeviceId(), e);
            log.error("检查MES确认状态异常: deviceId={}", deviceConfig.getDeviceId(), e);
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("检查MES确认状态异常: " + e.getMessage())
                    .data(data)
                    .build();
        }
    }
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
@@ -38,6 +38,7 @@
        config.setScanIntervalMs(getLogicParam(logicParams, "scanIntervalMs", 10_000));
        config.setTransferDelayMs(getLogicParam(logicParams, "transferDelayMs", 30_000));
        config.setVehicleCapacity(getLogicParam(logicParams, "vehicleCapacity", 6000));
        config.setGlassGap(getLogicParam(logicParams, "glassGap", 200));
        return config;
    }
@@ -68,10 +69,11 @@
        defaults.put("scanIntervalMs", 10_000);
        defaults.put("transferDelayMs", 30_000);
        defaults.put("vehicleCapacity", 6_000);
        defaults.put("glassGap", 200);
        try {
            return objectMapper.writeValueAsString(defaults);
        } catch (JsonProcessingException e) {
            return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000}";
            return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000,\"glassGap\":200}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java
@@ -23,5 +23,10 @@
     * 可装载的最大宽度(mm)
     */
    private Integer vehicleCapacity = 6_000;
    /**
     * 玻璃之间的物理间隔(mm)
     */
    private Integer glassGap = 200;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -112,6 +112,8 @@
        if (mesData == null || mesData.isEmpty()) {
            log.error("等待MES写入玻璃信息超时: deviceId={}, timeout={}ms", 
                    deviceConfig.getId(), config.getScanIntervalMs());
            // 超时也要清空plcRequest
            clearPlcRequestFields(deviceConfig, serializer);
            return buildResult(deviceConfig, "scanOnce", false,
                    String.format("等待MES写入玻璃信息超时(%dms)", config.getScanIntervalMs()), null);
        }
@@ -119,6 +121,8 @@
        // 3. 读取MES回写的玻璃信息
        String glassId = parseString(mesData.get("mesGlassId"));
        if (!StringUtils.hasText(glassId)) {
            // MES未提供玻璃ID也要清空plcRequest
            clearPlcRequestFields(deviceConfig, serializer);
            return buildResult(deviceConfig, "scanOnce", false, "MES写区未提供玻璃ID", null);
        }
        // 读取MES尺寸数据:mesWidth=表宽,mesHeight=长
@@ -276,7 +280,7 @@
        }
        glassInfo.setStatus(GlassInfo.Status.PENDING);
        if (workLine != null) {
            glassInfo.setDescription("workLine=" + workLine);
            glassInfo.setWorkLine(workLine);
        }
        Date now = new Date();
        glassInfo.setCreatedTime(now);
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -16,9 +16,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PreDestroy;
import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
@@ -92,6 +91,8 @@
                    return handleStopMonitor(deviceConfig);
                case "clearBuffer":
                    return handleClearBuffer(deviceConfig);
                case "clearPlc":
                    return handleClearPlc(deviceConfig);
                default:
                    return buildResult(deviceConfig, operation, false, 
                            "不支持的操作: " + operation);
@@ -124,46 +125,66 @@
        try {
            // 1. 从数据库查询最近扫码的玻璃信息(最近1分钟内的记录)
            List<GlassInfo> recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams);
            if (recentGlasses.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "暂无待处理的玻璃信息");
            boolean hasNewGlass = false;
            if (!recentGlasses.isEmpty()) {
                log.info("查询到最近扫码的玻璃: deviceId={}, count={}",
                        deviceId, recentGlasses.size());
                // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间
                hasNewGlass = updateBuffer(deviceId, recentGlasses);
                if (hasNewGlass) {
                    lastScanTime
                            .computeIfAbsent(deviceId, k -> new AtomicLong())
                            .set(System.currentTimeMillis());
                }
            } else {
                log.debug("未查询到最近扫码的玻璃: deviceId={}", deviceId);
            }
            log.info("查询到最近扫码的玻璃: deviceId={}, count={}",
                    deviceId, recentGlasses.size());
            // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间
            boolean hasNewGlass = updateBuffer(deviceId, recentGlasses);
            if (hasNewGlass) {
                lastScanTime
                        .computeIfAbsent(deviceId, k -> new AtomicLong())
                        .set(System.currentTimeMillis());
            }
            // 3. 检查是否需要立即处理(容量已满或30s内无新玻璃)
            // 3. 检查缓冲队列(即使查询不到新玻璃,缓冲中可能还有待处理的玻璃)
            List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
            if (buffer == null || buffer.isEmpty()) {
                // 缓冲为空且无新玻璃,返回空状态
                return buildResult(deviceConfig, "checkAndProcess", true, 
                        "缓冲队列为空");
                        "缓冲队列为空,无待处理玻璃");
            }
            // 4. 判断是否满足处理条件
            boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config);
            if (!shouldProcess) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "等待更多玻璃或30s超时");
                // 未满足处理条件:构造带有等待进度的提示信息,便于前端展示
                String waitMessage;
                AtomicLong lastTime = lastScanTime.get(deviceId);
                Integer delayMs = config.getTransferDelayMs();
                if (lastTime != null && delayMs != null && delayMs > 0) {
                    long elapsedMs = System.currentTimeMillis() - lastTime.get();
                    if (elapsedMs < 0) {
                        elapsedMs = 0;
                    }
                    long totalMs = delayMs;
                    long elapsedSec = elapsedMs / 1000;
                    long totalSec = totalMs / 1000;
                    waitMessage = String.format("等待更多玻璃或超时触发批次处理 (已等待 %d/%d 秒)",
                            elapsedSec, totalSec);
                } else {
                    // 没有有效的最后扫码时间或配置,退回到固定提示
                    waitMessage = "等待更多玻璃或30s超时";
                }
                return buildResult(deviceConfig, "checkAndProcess", true, waitMessage);
            }
            // 5. 容量判断和批次组装
            List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity());
            // 5. 容量判断和批次组装(考虑玻璃间隙)
            Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
            List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity(), glassGap);
            if (batch.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", false, 
                        "无法组装有效批次(容量不足)");
            }
            // 6. 写入PLC
            // 6. 写入PLC(尝试从任务参数中获取卧转立编号)
            DevicePlcVO.OperationResult writeResult = writeBatchToPlc(
                    deviceConfig, batch, serializer, logicParams);
                    deviceConfig, batch, serializer, logicParams, params);
            
            if (!Boolean.TRUE.equals(writeResult.getSuccess())) {
                return writeResult;
@@ -198,9 +219,23 @@
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()),
                    GlassInfo.Status.PROCESSED);
            String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s",
                    batch.size(),
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            // 8. 检查缓冲是否为空,如果为空且无新玻璃,标记为完成
            List<GlassBufferItem> remainingBuffer = glassBuffer.get(deviceId);
            boolean bufferEmpty = remainingBuffer == null || remainingBuffer.isEmpty();
            boolean noNewGlass = !hasNewGlass;
            String msg;
            if (bufferEmpty && noNewGlass) {
                // 缓冲已清空且无新玻璃,任务完成
                msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s, 缓冲已清空,任务完成",
                        batch.size(),
                        batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            } else {
                // 缓冲还有玻璃或可能有新玻璃,继续运行
                msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s",
                        batch.size(),
                        batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            }
            return buildResult(deviceConfig, "checkAndProcess", true, msg);
        } catch (Exception e) {
@@ -223,8 +258,8 @@
        }
        
        try {
            // 从配置中获取workLine,用于过滤
            String workLine = getLogicParam(logicParams, "workLine", null);
            // 从配置中获取workLine,用于过滤(配置中是Integer类型)
            Integer workLine = getLogicParam(logicParams, "workLine", null);
            
            // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏)
            Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
@@ -235,9 +270,9 @@
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT 20"); // 限制查询数量,避免过多
            
            // 如果配置了workLine,则过滤description
            if (workLine != null && !workLine.isEmpty()) {
                wrapper.like(GlassInfo::getDescription, "workLine=" + workLine);
            // 如果配置了workLine,则过滤work_line字段
            if (workLine != null) {
                wrapper.eq(GlassInfo::getWorkLine, workLine);
            }
            
            List<GlassInfo> recentGlasses = glassInfoMapper.selectList(wrapper);
@@ -280,17 +315,21 @@
    /**
     * 判断是否应该处理批次
     * 注意:这里只做粗略判断,精确的容量计算(含间隙)在assembleBatch中完成
     */
    private boolean shouldProcessBatch(String deviceId, 
                                      List<GlassBufferItem> buffer,
                                      WorkstationLogicConfig config) {
        // 条件1:缓冲队列已满(达到容量限制)
        // 粗略计算:所有玻璃长度之和(不考虑间隙,因为间隙是动态的)
        int totalLength = buffer.stream()
                .mapToInt(item -> item.glassInfo.getGlassLength() != null ? 
                        item.glassInfo.getGlassLength() : 0)
                .sum();
        if (totalLength >= config.getVehicleCapacity()) {
            log.info("缓冲队列容量已满,触发批次处理: deviceId={}, totalLength={}, capacity={}",
        // 粗略判断:如果总长度接近容量(留一些余量给间隙),就触发处理
        // 精确判断会在assembleBatch中完成
        if (totalLength >= config.getVehicleCapacity() * 0.8) { // 80%阈值,留余量给间隙
            log.info("缓冲队列容量接近满载,触发批次处理: deviceId={}, totalLength={}, capacity={}",
                    deviceId, totalLength, config.getVehicleCapacity());
            return true;
        }
@@ -310,25 +349,50 @@
    }
    /**
     * 组装批次(容量判断)
     * 组装批次(容量判断,考虑玻璃间隙)
     * @param buffer 缓冲队列
     * @param vehicleCapacity 车辆容量(mm)
     * @param glassGap 玻璃之间的物理间隔(mm),默认200mm
     * @return 组装好的批次列表
     */
    private List<GlassInfo> assembleBatch(List<GlassBufferItem> buffer, 
                                          int vehicleCapacity) {
                                          int vehicleCapacity,
                                          int glassGap) {
        List<GlassInfo> batch = new ArrayList<>();
        int usedLength = 0;
        int gap = Math.max(glassGap, 0); // 确保间隔不为负数
        
        for (GlassBufferItem item : buffer) {
            GlassInfo glass = item.glassInfo;
            int glassLength = glass.getGlassLength() != null ? 
                    glass.getGlassLength() : 0;
            
            if (usedLength + glassLength <= vehicleCapacity && batch.size() < 6) {
                batch.add(glass);
                usedLength += glassLength;
            if (glassLength <= 0) {
                continue; // 跳过无效长度的玻璃
            }
            if (batch.isEmpty()) {
                // 第一块玻璃,不需要间隙
                if (glassLength <= vehicleCapacity && batch.size() < 6) {
                    batch.add(glass);
                    usedLength = glassLength;
                } else {
                    break; // 第一块就装不下
                }
            } else {
                break;
                // 后续玻璃需要考虑间隙:玻璃长度 + 间隙
                int requiredLength = glassLength + gap;
                if (usedLength + requiredLength <= vehicleCapacity && batch.size() < 6) {
                    batch.add(glass);
                    usedLength += requiredLength; // 包含间隙
                } else {
                    break; // 装不下了
                }
            }
        }
        log.debug("批次组装完成: batchSize={}, usedLength={}, capacity={}, glassGap={}",
                batch.size(), usedLength, vehicleCapacity, gap);
        
        return batch;
    }
@@ -340,7 +404,8 @@
            DeviceConfig deviceConfig,
            List<GlassInfo> batch,
            EnhancedS7Serializer serializer,
            Map<String, Object> logicParams) {
            Map<String, Object> logicParams,
            Map<String, Object> params) {
        
        Map<String, Object> payload = new HashMap<>();
        
@@ -354,10 +419,33 @@
        // 写入玻璃数量
        payload.put("plcGlassCount", count);
        
        // 写入位置信息(如果有配置)
        Integer inPosition = getLogicParam(logicParams, "inPosition", null);
        // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取)
        Integer inPosition = null;
        if (params != null) {
            try {
                Object ctxObj = params.get("_taskContext");
                if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) {
                    com.mes.task.model.TaskExecutionContext ctx =
                            (com.mes.task.model.TaskExecutionContext) ctxObj;
                    Object positionObj = ctx.getParameters().getExtra() != null
                            ? ctx.getParameters().getExtra().get("inPosition") : null;
                    if (positionObj instanceof Number) {
                        inPosition = ((Number) positionObj).intValue();
                    }
                }
            } catch (Exception e) {
                log.debug("从任务参数获取卧转立编号失败: deviceId={}", deviceConfig.getId(), e);
            }
        }
        // 如果任务参数中没有,从设备配置中获取
        if (inPosition == null) {
            inPosition = getLogicParam(logicParams, "inPosition", null);
        }
        if (inPosition != null) {
            payload.put("inPosition", inPosition);
            log.info("写入卧转立编号: deviceId={}, inPosition={}", deviceConfig.getId(), inPosition);
        } else {
            log.debug("未配置卧转立编号,跳过写入: deviceId={}", deviceConfig.getId());
        }
        
        // 写入请求字(触发大车)
@@ -365,8 +453,8 @@
        
        try {
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            log.info("批次已写入PLC: deviceId={}, glassCount={}",
                    deviceConfig.getId(), count);
            log.info("批次已写入PLC: deviceId={}, glassCount={}, inPosition={}",
                    deviceConfig.getId(), count, inPosition);
            return buildResult(deviceConfig, "writeBatchToPlc", true, 
                    "批次写入成功");
        } catch (Exception e) {
@@ -450,6 +538,34 @@
        log.info("已清空缓冲队列: deviceId={}", deviceId);
        return buildResult(deviceConfig, "clearBuffer", true, "缓冲队列已清空");
    }
    /**
     * 清空PLC相关字段(供测试页面一键清空使用)
     */
    private DevicePlcVO.OperationResult handleClearPlc(DeviceConfig deviceConfig) {
        try {
            EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
            if (serializer == null) {
                return buildResult(deviceConfig, "clearPlc", false, "获取PLC序列化器失败");
            }
            Map<String, Object> payload = new HashMap<>();
            // 根据卧转立主体写入的字段进行清空
            for (int i = 1; i <= 6; i++) {
                payload.put("plcGlassId" + i, "");
            }
            payload.put("plcGlassCount", 0);
            payload.put("plcRequest", 0);
            payload.put("inPosition", 0);
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            log.info("卧转立主体清空PLC字段完成: deviceId={}", deviceConfig.getId());
            return buildResult(deviceConfig, "clearPlc", true, "已清空卧转立主体PLC字段");
        } catch (Exception e) {
            log.error("卧转立主体清空PLC失败: deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, "clearPlc", false, "清空PLC失败: " + e.getMessage());
        }
    }
    /**
     * 构建操作结果
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java
@@ -570,7 +570,10 @@
            lowerName.startsWith("plcglassid")) {
            return EDataType.STRING;
        }
        // 联机状态等布尔标记
        if (lowerName.contains("online")) {
            return EDataType.BOOL;
        }
        // 默认返回UINT16
        return EDataType.UINT16;
    }
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -148,7 +148,7 @@
            
            for (DeviceConfig device : devices) {
                String deviceType = device.getDeviceType();
                log.info("处理设备: deviceId={}, deviceType={}, deviceName={}, WORKSTATION_SCANNER常量={}, equals={}",
                log.debug("处理设备: deviceId={}, deviceType={}, deviceName={}, WORKSTATION_SCANNER常量={}, equals={}",
                        device.getId(), deviceType, device.getDeviceName(), 
                        DeviceConfig.DeviceType.WORKSTATION_SCANNER,
                        DeviceConfig.DeviceType.WORKSTATION_SCANNER.equals(deviceType));
@@ -157,25 +157,20 @@
                        || (deviceType != null && (deviceType.contains("扫码") || deviceType.contains("SCANNER")));
                boolean isLargeGlass = DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType);
                boolean isTransfer = DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceType);
                log.info("设备类型判断: deviceId={}, isLoadVehicle={}, isScanner={}, isLargeGlass={}, isTransfer={}",
                log.debug("设备类型判断: deviceId={}, isLoadVehicle={}, isScanner={}, isLargeGlass={}, isTransfer={}",
                        device.getId(), isLoadVehicle, isScanner, isLargeGlass, isTransfer);
                // 1. 卧转立扫码设备:启动定时器扫描(每10秒处理一个玻璃ID)
                if (isScanner) {
                    log.info("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                    log.debug("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    
                    ScheduledFuture<?> scannerTask = startScannerTimer(task, step, device, context);
                    if (scannerTask != null) {
                        registerScheduledTask(task.getTaskId(), scannerTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,每10秒扫描一次"));
                        log.info("扫码设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                        log.debug("扫码设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                    } else {
                        log.warn("扫码设备定时器启动失败,glassIds可能为空: deviceId={}, taskId={}, contextParams={}", 
                                device.getId(), task.getTaskId(), context.getParameters());
@@ -190,20 +185,15 @@
                // 2. 卧转立设备:启动定时器定期检查并处理(中转设备)
                if (isTransfer) {
                    log.info("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                    log.debug("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    
                    ScheduledFuture<?> transferTask = startTransferTimer(task, step, device, context);
                    if (transferTask != null) {
                        registerScheduledTask(task.getTaskId(), transferTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,定期检查并处理玻璃批次"));
                        log.info("卧转立设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                        log.debug("卧转立设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                    } else {
                        log.warn("卧转立设备定时器启动失败: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                        stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
@@ -221,11 +211,6 @@
                    boolean isInboundVehicle = currentLoadVehicleIndex == 1; // 第一个大车是进片大车
                    
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    
                    ScheduledFuture<?> vehicleTask;
                    if (isInboundVehicle) {
@@ -260,11 +245,6 @@
                // 4. 大理片笼设备:启动定时器逻辑处理(不涉及PLC交互,只负责逻辑处理)
                if (isLargeGlass) {
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    
                    ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, step, device, context);
                    if (largeGlassTask != null) {
@@ -296,7 +276,7 @@
            // 定时器会在后台持续运行,直到手动停止或超时
            boolean hasScheduledTasks = !CollectionUtils.isEmpty(taskScheduledTasks.get(task.getTaskId()));
            if (hasScheduledTasks) {
                log.info("任务已启动所有定时器,保持运行状态: taskId={}, scheduledTasksCount={}",
                log.debug("任务已启动所有定时器,保持运行状态: taskId={}, scheduledTasksCount={}",
                        task.getTaskId(), taskScheduledTasks.get(task.getTaskId()).size());
                // 任务保持 RUNNING 状态,定时器在后台运行
                // 不更新任务状态为 COMPLETED,让任务持续运行
@@ -376,7 +356,7 @@
        try {
            TaskParameters params = context.getParameters();
            List<String> glassIds = params.getGlassIds();
            log.info("卧转立扫码定时器初始化: taskId={}, deviceId={}, glassIds={}, glassIdsSize={}, isEmpty={}",
            log.debug("卧转立扫码定时器初始化: taskId={}, deviceId={}, glassIds={}, glassIdsSize={}, isEmpty={}",
                    task.getTaskId(), device.getId(), glassIds, 
                    glassIds != null ? glassIds.size() : 0, 
                    CollectionUtils.isEmpty(glassIds));
@@ -391,19 +371,22 @@
            AtomicInteger successCount = new AtomicInteger(0);
            AtomicInteger failCount = new AtomicInteger(0);
            
            final long CYCLE_INTERVAL_MS = 10_000; // 10秒间隔
            // 从设备配置中获取扫码间隔,默认10秒
            Map<String, Object> logicParams = parseLogicParams(device);
            Integer scanIntervalMs = getLogicParam(logicParams, "scanIntervalMs", 10_000);
            
            log.info("启动卧转立扫码定时器: taskId={}, deviceId={}, glassCount={}, interval={}s, glassIds={}",
                    task.getTaskId(), device.getId(), glassIds.size(), CYCLE_INTERVAL_MS / 1000, glassIds);
            log.debug("启动卧转立扫码定时器: taskId={}, deviceId={}, glassCount={}, interval={}ms, glassIds={}",
                    task.getTaskId(), device.getId(), glassIds.size(), scanIntervalMs, glassIds);
            
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止卧转立扫码定时器: taskId={}, deviceId={}",
                        log.debug("任务已取消,停止卧转立扫码定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    ensureStepRunning(step, task.getTaskId());
                    // 检查是否需要暂停
                    if (shouldPauseScanner(context)) {
                        log.debug("卧转立扫码定时器暂停: taskId={}, deviceId={}", task.getTaskId(), device.getId());
@@ -413,9 +396,25 @@
                    // 检查是否还有待处理的玻璃ID
                    String glassId = glassIdQueue.poll();
                    if (glassId == null) {
                        log.info("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}",
                        log.debug("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}",
                                task.getTaskId(), device.getId(), processedCount.get(), glassIds.size(),
                                successCount.get(), failCount.get());
                        // 清空plcRequest和plcGlassId(确保PLC状态清理)
                        try {
                            DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                            if (handler != null) {
                                Map<String, Object> clearParams = new HashMap<>();
                                clearParams.put("_taskContext", context);
                                handler.execute(device, "clearPlc", clearParams);
                                log.debug("卧转立扫码定时器完成,已清空PLC请求字段: taskId={}, deviceId={}",
                                        task.getTaskId(), device.getId());
                            }
                        } catch (Exception e) {
                            log.warn("卧转立扫码定时器完成时清空PLC失败: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
                        }
                        // 若之前未出现失败,再将状态置为完成
                        boolean alreadyFailed = TaskStepDetail.Status.FAILED.name().equals(step.getStatus());
                        if (!alreadyFailed) {
@@ -426,6 +425,8 @@
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                            // 扫码设备完成后尝试自动收尾整个任务
                            checkAndCompleteTaskIfDone(step.getTaskId());
                        }
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.COMPLETED, context);
@@ -433,32 +434,31 @@
                    }
                    
                    int currentIndex = processedCount.incrementAndGet();
                    log.info("卧转立扫码定时器处理第{}/{}个玻璃: taskId={}, deviceId={}, glassId={}",
                    log.debug("卧转立扫码定时器处理第{}/{}个玻璃: taskId={}, deviceId={}, glassId={}",
                            currentIndex, glassIds.size(), task.getTaskId(), device.getId(), glassId);
                    
                    // 执行单次扫描
                    Map<String, Object> scanParams = new HashMap<>();
                    scanParams.put("glassId", glassId);
                    scanParams.put("_taskContext", context);
                    log.info("卧转立扫码定时器准备执行: taskId={}, deviceId={}, glassId={}, scanParams={}",
                    log.debug("卧转立扫码定时器准备执行: taskId={}, deviceId={}, glassId={}, scanParams={}",
                            task.getTaskId(), device.getId(), glassId, scanParams);
                    
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                    if (handler != null) {
                        // 将logicParams合并到scanParams中
                        Map<String, Object> logicParams = parseLogicParams(device);
                        // 将logicParams合并到scanParams中(使用已定义的logicParams变量)
                        if (logicParams != null && !logicParams.isEmpty()) {
                            scanParams.put("_logicParams", logicParams);
                        }
                        log.info("卧转立扫码定时器调用handler.execute: taskId={}, deviceId={}, glassId={}, operation=scanOnce, scanParamsKeys={}, scanParams={}",
                        log.debug("卧转立扫码定时器调用handler.execute: taskId={}, deviceId={}, glassId={}, operation=scanOnce, scanParamsKeys={}, scanParams={}",
                                task.getTaskId(), device.getId(), glassId, scanParams.keySet(), scanParams);
                        DevicePlcVO.OperationResult result = handler.execute(device, "scanOnce", scanParams);
                        log.info("卧转立扫码定时器handler.execute返回: taskId={}, deviceId={}, glassId={}, success={}",
                        log.debug("卧转立扫码定时器handler.execute返回: taskId={}, deviceId={}, glassId={}, success={}",
                                task.getTaskId(), device.getId(), glassId, result.getSuccess());
                        
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            successCount.incrementAndGet();
                            log.info("卧转立扫码定时器处理成功: taskId={}, deviceId={}, glassId={}",
                            log.debug("卧转立扫码定时器处理成功: taskId={}, deviceId={}, glassId={}",
                                    task.getTaskId(), device.getId(), glassId);
                        } else {
                            failCount.incrementAndGet();
@@ -466,9 +466,10 @@
                                    task.getTaskId(), device.getId(), glassId, result.getMessage());
                        }
                        
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        // 通知步骤更新(让前端实时看到步骤状态)
                        // 更新步骤状态(显示进度,保持RUNNING状态直到所有玻璃处理完成)
                        updateStepStatusForScanner(step, result, currentIndex, glassIds.size(),
                                successCount.get(), failCount.get());
                        // 通知步骤更新(让前端实时看到步骤状态和进度)
                        notificationService.notifyStepUpdate(task.getTaskId(), step);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
@@ -481,7 +482,7 @@
                    log.error("卧转立扫码定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                    failCount.incrementAndGet();
                }
            }, 0, CYCLE_INTERVAL_MS, TimeUnit.MILLISECONDS);
            }, 0, scanIntervalMs, TimeUnit.MILLISECONDS);
            
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
@@ -504,17 +505,18 @@
            Map<String, Object> logicParams = parseLogicParams(device);
            Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs", 5_000);
            
            log.info("启动卧转立设备定时器: taskId={}, deviceId={}, interval={}ms",
            log.debug("启动卧转立设备定时器: taskId={}, deviceId={}, interval={}ms",
                    task.getTaskId(), device.getId(), monitorIntervalMs);
            
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止卧转立设备定时器: taskId={}, deviceId={}",
                        log.debug("任务已取消,停止卧转立设备定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    ensureStepRunning(step, task.getTaskId());
                    // 构建参数
                    Map<String, Object> params = new HashMap<>();
                    params.put("_taskContext", context);
@@ -536,10 +538,10 @@
                        if (opSuccess) {
                            String message = result.getMessage();
                            if (message != null && message.contains("批次已写入PLC")) {
                                log.info("卧转立设备定时器执行成功(已写入PLC): taskId={}, deviceId={}, message={}",
                        log.debug("卧转立设备定时器执行成功(已写入PLC): taskId={}, deviceId={}, message={}",
                                        task.getTaskId(), device.getId(), message);
                            } else {
                                log.debug("卧转立设备定时器等待中: taskId={}, deviceId={}, message={}",
                        log.debug("卧转立设备定时器等待中: taskId={}, deviceId={}, message={}",
                                        task.getTaskId(), device.getId(), message);
                            }
                        } else {
@@ -574,17 +576,18 @@
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            final AtomicInteger lastProcessedCount = new AtomicInteger(0);
            
            log.info("启动进片大车设备定时器: taskId={}, deviceId={}, interval={}s",
            log.debug("启动进片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止进片大车定时器: taskId={}, deviceId={}",
                        log.debug("任务已取消,停止进片大车定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    ensureStepRunning(step, task.getTaskId());
                    // 检查是否有卧转立主体已输出、准备上大车的玻璃信息
                    List<String> readyGlassIds = getTransferReadyGlassIds(context);
                    if (CollectionUtils.isEmpty(readyGlassIds)) {
@@ -600,7 +603,7 @@
                        return;
                    }
                    
                    log.info("进片大车设备定时器检测到卧转立输出的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                        log.debug("进片大车设备定时器检测到卧转立输出的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), currentCount);
                    
                    // 检查容量
@@ -615,10 +618,11 @@
                        if (logicParams != null && !logicParams.isEmpty()) {
                            checkParams.put("_logicParams", logicParams);
                        }
                        DevicePlcVO.OperationResult result = handler.execute(device, "feedGlass", checkParams);
                        // 第一步:写入大车上料请求
                        DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams);
                        
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            log.info("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                        if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                            log.debug("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), readyGlassIds.size());
                            // 将已装载的玻璃ID保存到共享数据中(供大理片笼使用)
                            setLoadedGlassIds(context, new ArrayList<>(readyGlassIds));
@@ -630,17 +634,36 @@
                        } else {
                            // 装不下,记录容量不足(是否需要影响扫码由工艺再决定)
                            log.warn("进片大车设备定时器容量不足: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                                    task.getTaskId(), device.getId(), feedResult.getMessage());
                            lastProcessedCount.set(currentCount); // 记录当前数量,避免重复检查
                        }
                        
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (!opSuccess) {
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        // 第二步:检查MES确认状态(如果大车处理器支持的话)
                        DevicePlcVO.OperationResult mesResult = null;
                        try {
                            mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                        } catch (Exception e) {
                            log.warn("进片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
                        }
                        // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                        if (mesResult != null) {
                            updateStepStatusForVehicle(step, mesResult);
                            boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        } else {
                            updateStepStatusForVehicle(step, feedResult);
                            boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        }
                    }
                } catch (Exception e) {
@@ -667,17 +690,18 @@
        try {
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            
            log.info("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s",
            log.debug("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止出片大车定时器: taskId={}, deviceId={}",
                        log.debug("任务已取消,停止出片大车定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    ensureStepRunning(step, task.getTaskId());
                    // 检查是否有已处理的玻璃信息(从大理片笼来的)
                    List<String> processedGlassIds = getProcessedGlassIds(context);
                    if (CollectionUtils.isEmpty(processedGlassIds)) {
@@ -686,7 +710,7 @@
                        return;
                    }
                    
                    log.info("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                    log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), processedGlassIds.size());
                    
                    // 执行出片操作
@@ -701,25 +725,45 @@
                        if (logicParams != null && !logicParams.isEmpty()) {
                            checkParams.put("_logicParams", logicParams);
                        }
                        DevicePlcVO.OperationResult result = handler.execute(device, "feedGlass", checkParams);
                        // 第一步:写入大车出片请求
                        DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams);
                        
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            log.info("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                        if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                            log.debug("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), processedGlassIds.size());
                            // 清空已处理的玻璃ID列表(已处理)
                            clearProcessedGlassIds(context);
                        } else {
                            log.debug("出片大车设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                                    task.getTaskId(), device.getId(), feedResult.getMessage());
                        }
                        
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (!opSuccess) {
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        // 第二步:检查MES确认状态(如果大车处理器支持的话)
                        DevicePlcVO.OperationResult mesResult = null;
                        try {
                            mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                        } catch (Exception e) {
                            log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
                        }
                        // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                        if (mesResult != null) {
                            updateStepStatusForVehicle(step, mesResult);
                            boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        } else {
                            updateStepStatusForVehicle(step, feedResult);
                            boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        }
                    }
                } catch (Exception e) {
@@ -749,17 +793,18 @@
            Integer processTimeSeconds = getLogicParam(logicParams, "processTimeSeconds", 30);
            final long PROCESS_TIME_MS = processTimeSeconds * 1000;
            
            log.info("启动大理片笼设备定时器: taskId={}, deviceId={}, processTime={}s",
            log.debug("启动大理片笼设备定时器: taskId={}, deviceId={}, processTime={}s",
                    task.getTaskId(), device.getId(), processTimeSeconds);
            
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止大理片笼定时器: taskId={}, deviceId={}",
                        log.debug("任务已取消,停止大理片笼定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    ensureStepRunning(step, task.getTaskId());
                    // 检查是否有已装载的玻璃信息(从进片大车来的)
                    List<String> loadedGlassIds = getLoadedGlassIds(context);
                    if (CollectionUtils.isEmpty(loadedGlassIds)) {
@@ -773,7 +818,7 @@
                    if (processStartTime == null) {
                        // 第一次检测到玻璃,记录开始处理时间
                        setProcessStartTime(context, System.currentTimeMillis());
                        log.info("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                        log.debug("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                                task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds);
                        return;
                    }
@@ -787,7 +832,7 @@
                    }
                    
                    // 处理时间已到,完成任务汇报
                    log.info("大理片笼设备处理完成: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                    log.debug("大理片笼设备处理完成: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                            task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds);
                    
                    // 将已处理的玻璃ID转移到已处理列表(供出片大车使用)
@@ -800,6 +845,8 @@
                    step.setErrorMessage(null);
                    step.setOutputData(toJson(Collections.singletonMap("glassIds", loadedGlassIds)));
                    taskStepDetailMapper.updateById(step);
                    // 大理片笼完成后尝试自动收尾整个任务
                    checkAndCompleteTaskIfDone(step.getTaskId());
                    
                } catch (Exception e) {
                    log.error("大理片笼设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
@@ -1024,7 +1071,7 @@
                    future.cancel(false);
                }
            }
            log.info("已停止任务的所有定时器: taskId={}, count={}", taskId, futures.size());
            log.debug("已停止任务的所有定时器: taskId={}, count={}", taskId, futures.size());
        }
        runningTaskContexts.remove(taskId);
    }
@@ -1040,7 +1087,7 @@
        long timeoutMs = timeoutMinutes * 60 * 1000;
        long deadline = System.currentTimeMillis() + timeoutMs;
        
        log.info("等待定时器任务完成: taskId={}, timeout={}分钟", taskId, timeoutMinutes);
        log.debug("等待定时器任务完成: taskId={}, timeout={}分钟", taskId, timeoutMinutes);
        
        while (System.currentTimeMillis() < deadline) {
            List<ScheduledFuture<?>> futures = taskScheduledTasks.get(taskId);
@@ -1069,7 +1116,54 @@
            }
        }
        
        log.info("定时器任务等待完成: taskId={}", taskId);
        log.debug("定时器任务等待完成: taskId={}", taskId);
    }
    /**
     * 当某个步骤可能完成时,检查任务是否所有步骤都已完成,如果是则自动将任务标记为已完成
     */
    private void checkAndCompleteTaskIfDone(String taskId) {
        if (taskId == null) {
            return;
        }
        try {
            MultiDeviceTask task = multiDeviceTaskMapper.selectOne(
                    Wrappers.<MultiDeviceTask>lambdaQuery()
                            .eq(MultiDeviceTask::getTaskId, taskId)
            );
            if (task == null) {
                return;
            }
            // 仅在任务仍为RUNNING时才尝试自动收尾
            if (!MultiDeviceTask.Status.RUNNING.name().equals(task.getStatus())) {
                return;
            }
            int totalSteps = task.getTotalSteps() != null ? task.getTotalSteps() : 0;
            if (totalSteps <= 0) {
                return;
            }
            int completedSteps = countCompletedSteps(taskId);
            if (completedSteps < totalSteps) {
                return;
            }
            // 所有步骤都已完成,收尾任务
            task.setStatus(MultiDeviceTask.Status.COMPLETED.name());
            task.setEndTime(new Date());
            multiDeviceTaskMapper.updateById(task);
            // 停止所有定时器
            stopScheduledTasks(taskId);
            // 通知任务完成
            notificationService.notifyTaskStatus(task);
            log.info("所有步骤已完成,自动将任务标记为已完成: taskId={}, totalSteps={}", taskId, totalSteps);
        } catch (Exception e) {
            log.warn("检查并自动完成任务失败: taskId={}", taskId, e);
        }
    }
    
    /**
@@ -1105,6 +1199,135 @@
    }
    
    /**
     * 确保步骤进入RUNNING状态(仅在第一次真正执行前调用)
     */
    private void ensureStepRunning(TaskStepDetail step, String taskId) {
        if (step == null) {
            return;
        }
        if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
            step.setStatus(TaskStepDetail.Status.RUNNING.name());
            if (step.getStartTime() == null) {
                step.setStartTime(new Date());
            }
            taskStepDetailMapper.updateById(step);
            notificationService.notifyStepUpdate(taskId, step);
        }
    }
    /**
     * 更新扫码设备步骤状态(显示进度,保持RUNNING状态直到所有玻璃处理完成)
     */
    private void updateStepStatusForScanner(TaskStepDetail step, DevicePlcVO.OperationResult result,
                                            int currentIndex, int totalCount,
                                            int successCount, int failCount) {
        if (step == null || result == null) {
            return;
        }
        boolean success = Boolean.TRUE.equals(result.getSuccess());
        // 保持RUNNING状态,直到所有玻璃处理完成(在定时器完成时再设置为COMPLETED)
        step.setStatus(TaskStepDetail.Status.RUNNING.name());
        // 更新时间和耗时,前端可以实时看到执行耗时
        Date now = new Date();
        if (step.getStartTime() == null) {
            step.setStartTime(now);
        }
        if (step.getStartTime() != null) {
            step.setDurationMs(now.getTime() - step.getStartTime().getTime());
        }
        // 更新进度信息
        String progressMessage = String.format("正在处理 %d/%d (成功:%d, 失败:%d)",
                currentIndex, totalCount, successCount, failCount);
        if (success) {
            // 成功时显示进度和成功消息
            String resultMessage = result.getMessage();
            if (StringUtils.hasText(resultMessage)) {
                step.setSuccessMessage(progressMessage + " - " + resultMessage);
            } else {
                step.setSuccessMessage(progressMessage);
            }
            step.setErrorMessage(null);
        } else {
            // 失败时显示进度和错误消息
            String errorMessage = result.getMessage();
            step.setErrorMessage(progressMessage + " - " + (StringUtils.hasText(errorMessage) ? errorMessage : "处理失败"));
            step.setSuccessMessage(null);
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    /**
     * 更新大车设备步骤状态(保持RUNNING,直到手动停止或任务取消;失败时标记为FAILED)
     */
    private void updateStepStatusForVehicle(TaskStepDetail step, DevicePlcVO.OperationResult result) {
        if (step == null || result == null) {
            return;
        }
        boolean success = Boolean.TRUE.equals(result.getSuccess());
        boolean completed = false;
        if (result.getData() != null && result.getData().get("completed") != null) {
            Object flag = result.getData().get("completed");
            if (flag instanceof Boolean) {
                completed = (Boolean) flag;
            } else {
                completed = "true".equalsIgnoreCase(String.valueOf(flag));
            }
        }
        Date now = new Date();
        // 初始化开始时间
        if (step.getStartTime() == null) {
            step.setStartTime(now);
        }
        if (success && !completed) {
            // 成功但未完成:保持RUNNING状态,仅更新提示信息和耗时
            step.setStatus(TaskStepDetail.Status.RUNNING.name());
            String message = result.getMessage();
            step.setSuccessMessage(StringUtils.hasText(message) ? message : "大车设备运行中");
            step.setErrorMessage(null);
            if (step.getStartTime() != null) {
                step.setDurationMs(now.getTime() - step.getStartTime().getTime());
            }
        } else if (success && completed) {
            // 成功且MES已确认完成:标记为COMPLETED并记录结束时间
            step.setStatus(TaskStepDetail.Status.COMPLETED.name());
            String message = result.getMessage();
            step.setSuccessMessage(StringUtils.hasText(message) ? message : "大车设备任务已完成");
            step.setErrorMessage(null);
            if (step.getEndTime() == null) {
                step.setEndTime(now);
            }
            if (step.getStartTime() != null && step.getEndTime() != null) {
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            }
            // 尝试自动收尾整个任务
            checkAndCompleteTaskIfDone(step.getTaskId());
        } else {
            // 失败:标记为FAILED并记录结束时间
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            String message = result.getMessage();
            step.setErrorMessage(message);
            if (step.getEndTime() == null) {
                step.setEndTime(now);
            }
            if (step.getStartTime() != null && step.getEndTime() != null) {
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            }
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    /**
     * 更新卧转立设备步骤状态(区分等待中和真正完成)
     */
    private void updateStepStatusForTransfer(TaskStepDetail step, DevicePlcVO.OperationResult result) {
@@ -1114,8 +1337,12 @@
        boolean success = Boolean.TRUE.equals(result.getSuccess());
        String message = result.getMessage();
        
        // 判断是否真正完成(只有写入PLC才算完成)
        boolean isRealCompleted = success && message != null && message.contains("批次已写入PLC");
        // 判断是否真正完成:
        // 1. 写入PLC成功
        // 2. 且缓冲已清空(表示所有玻璃已处理完,无新玻璃)
        boolean isRealCompleted = success && message != null
                && message.contains("批次已写入PLC")
                && message.contains("缓冲已清空,任务完成");
        
        if (isRealCompleted) {
            // 真正完成:设置为完成状态,并设置结束时间
@@ -1123,6 +1350,23 @@
            step.setSuccessMessage(message);
            if (step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
            // 计算耗时
            if (step.getStartTime() != null && step.getEndTime() != null) {
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            }
            log.debug("卧转立设备步骤已完成: stepId={}, durationMs={}", step.getId(), step.getDurationMs());
            // 卧转立主体完成后尝试自动收尾整个任务
            checkAndCompleteTaskIfDone(step.getTaskId());
        } else if (success && message != null && message.contains("批次已写入PLC")) {
            // 写入PLC成功但缓冲还有玻璃(车满情况),继续运行
            if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                step.setStatus(TaskStepDetail.Status.RUNNING.name());
            }
            step.setSuccessMessage(message);
            // 确保开始时间已设置
            if (step.getStartTime() == null) {
                step.setStartTime(new Date());
            }
        } else if (success) {
            // 等待中:保持运行状态,只更新消息
@@ -1140,6 +1384,10 @@
            step.setErrorMessage(message);
            if (step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
            // 计算耗时
            if (step.getStartTime() != null && step.getEndTime() != null) {
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            }
        }
        
@@ -1368,7 +1616,7 @@
    }
    /**
     * 分批执行大车设备玻璃上料(当玻璃ID数量超过6个且设置了单片间隔时)
     * 分批执行大车设备玻璃上料(当玻璃ID数量超过6个时)
     */
    private StepResult executeLoadVehicleWithBatches(MultiDeviceTask task,
                                                      DeviceConfig device,
@@ -1376,13 +1624,12 @@
                                                      TaskExecutionContext context,
                                                      List<Map<String, Object>> stepSummaries) {
        List<String> allGlassIds = context.getParameters().getGlassIds();
        Integer glassIntervalMs = context.getParameters().getGlassIntervalMs();
        int batchSize = 6; // 每批最多6个玻璃ID
        
        // 分批处理
        int totalBatches = (allGlassIds.size() + batchSize - 1) / batchSize;
        log.info("大车设备分批上料: deviceId={}, totalGlassIds={}, batchSize={}, totalBatches={}, glassIntervalMs={}",
                device.getId(), allGlassIds.size(), batchSize, totalBatches, glassIntervalMs);
        log.debug("大车设备分批上料: deviceId={}, totalGlassIds={}, batchSize={}, totalBatches={}",
                device.getId(), allGlassIds.size(), batchSize, totalBatches);
        
        for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
            int startIndex = batchIndex * batchSize;
@@ -1392,7 +1639,6 @@
            // 创建临时参数,只包含当前批次的玻璃ID
            TaskParameters batchParams = new TaskParameters();
            batchParams.setGlassIds(new ArrayList<>(batchGlassIds));
            batchParams.setGlassIntervalMs(glassIntervalMs);
            batchParams.setPositionCode(context.getParameters().getPositionCode());
            batchParams.setPositionValue(context.getParameters().getPositionValue());
            
@@ -1413,20 +1659,8 @@
                return stepResult;
            }
            
            log.info("大车设备分批上料成功: deviceId={}, batchIndex={}/{}, glassIds={}",
            log.debug("大车设备分批上料成功: deviceId={}, batchIndex={}/{}, glassIds={}",
                    device.getId(), batchIndex + 1, totalBatches, batchGlassIds);
            // 如果不是最后一批,等待间隔(模拟玻璃每片运动的时间)
            // 这个等待让大车有时间处理当前批次的玻璃,然后再传递下一批
            if (batchIndex < totalBatches - 1 && glassIntervalMs != null && glassIntervalMs > 0) {
                try {
                    log.info("等待单片间隔(模拟玻璃运动时间): glassIntervalMs={}ms, 大车可在此期间继续装玻璃", glassIntervalMs);
                    Thread.sleep(glassIntervalMs);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return StepResult.failure(device.getDeviceName(), "等待单片间隔时被中断");
                }
            }
        }
        
        // 更新上下文中的已加载玻璃ID
@@ -1487,7 +1721,7 @@
        Map<String, Object> params = buildOperationParams(device, context);
        // 将context引用放入params,供设备处理器使用(用于设备协调)
        params.put("_taskContext", context);
        log.info("executeStepWithRetry构建参数: deviceId={}, deviceType={}, operation={}, paramsKeys={}, params={}",
        log.debug("executeStepWithRetry构建参数: deviceId={}, deviceType={}, operation={}, paramsKeys={}, params={}",
                device.getId(), device.getDeviceType(), determineOperation(device, params), params.keySet(), params);
        step.setInputData(toJson(params));
        taskStepDetailMapper.updateById(step);
@@ -1503,7 +1737,7 @@
                if (retryAttempt > 0) {
                    // 重试前等待
                    long waitTime = retryPolicy.calculateRetryInterval(retryAttempt);
                    log.info("步骤执行重试: deviceId={}, operation={}, retryAttempt={}/{}, waitTime={}ms",
                    log.debug("步骤执行重试: deviceId={}, operation={}, retryAttempt={}/{}, waitTime={}ms",
                        device.getId(), operation, retryAttempt, retryPolicy.getMaxRetryCount(), waitTime);
                    Thread.sleep(waitTime);
                    
@@ -1697,7 +1931,7 @@
            try {
                if (retryAttempt > 0) {
                    long waitTime = retryPolicy.calculateRetryInterval(retryAttempt);
                    log.info("交互步骤执行重试: deviceId={}, retryAttempt={}/{}, waitTime={}ms",
                    log.debug("交互步骤执行重试: deviceId={}, retryAttempt={}/{}, waitTime={}ms",
                        device.getId(), retryAttempt, retryPolicy.getMaxRetryCount(), waitTime);
                    Thread.sleep(waitTime);
                    
@@ -1914,10 +2148,6 @@
                if (taskParams.getPositionValue() != null) {
                    params.put("positionValue", taskParams.getPositionValue());
                }
                // 传递单片间隔配置,如果任务参数中有设置,优先使用任务参数的,否则使用设备配置的
                if (taskParams.getGlassIntervalMs() != null) {
                    params.put("glassIntervalMs", taskParams.getGlassIntervalMs());
                }
                params.put("triggerRequest", true);
                break;
            case DeviceConfig.DeviceType.LARGE_GLASS:
@@ -1935,13 +2165,13 @@
            case DeviceConfig.DeviceType.WORKSTATION_SCANNER:
                // 卧转立扫码设备:从任务参数中获取玻璃ID列表,取第一个作为当前要测试的玻璃ID
                // 注意:扫码设备通常通过定时器执行,但如果通过executeStep执行,也需要传递glassId
                log.info("buildOperationParams处理扫码设备: deviceId={}, taskParams.glassIds={}, isEmpty={}",
                log.debug("buildOperationParams处理扫码设备: deviceId={}, taskParams.glassIds={}, isEmpty={}",
                        device.getId(), taskParams.getGlassIds(), 
                        CollectionUtils.isEmpty(taskParams.getGlassIds()));
                if (!CollectionUtils.isEmpty(taskParams.getGlassIds())) {
                    params.put("glassId", taskParams.getGlassIds().get(0));
                    params.put("glassIds", new ArrayList<>(taskParams.getGlassIds()));
                    log.info("buildOperationParams为扫码设备添加glassId: deviceId={}, glassId={}, glassIdsSize={}",
                    log.debug("buildOperationParams为扫码设备添加glassId: deviceId={}, glassId={}, glassIdsSize={}",
                            device.getId(), taskParams.getGlassIds().get(0), taskParams.getGlassIds().size());
                } else {
                    log.warn("buildOperationParams扫码设备glassIds为空: deviceId={}, taskParams.glassIds={}, taskParams={}", 
@@ -2017,7 +2247,7 @@
        if (!CollectionUtils.isEmpty(scannerGlassIds)) {
            context.getParameters().setGlassIds(new ArrayList<>(scannerGlassIds));
            context.setLoadedGlassIds(new ArrayList<>(scannerGlassIds));
            log.info("卧转立扫码获取到玻璃ID: {}", scannerGlassIds);
            log.debug("卧转立扫码获取到玻璃ID: {}", scannerGlassIds);
        } else {
            log.warn("卧转立扫码未获取到玻璃ID,后续设备可能无法执行");
        }
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java
@@ -306,6 +306,7 @@
        data.put("durationMs", step.getDurationMs() != null ? step.getDurationMs() : 0);
        data.put("retryCount", step.getRetryCount() != null ? step.getRetryCount() : 0);
        data.put("errorMessage", step.getErrorMessage() != null ? step.getErrorMessage() : "");
        data.put("successMessage", step.getSuccessMessage() != null ? step.getSuccessMessage() : "");
        return data;
    }
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
@@ -32,16 +32,16 @@
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="玻璃间隔(秒)">
        <el-form-item label="玻璃间隙(mm)">
          <el-input-number
            v-model="glassIntervalSeconds"
            :min="0.1"
            :max="10"
            :step="0.1"
            :precision="1"
            v-model="config.glassGap"
            :min="0"
            :max="1000"
            :step="10"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">玻璃上料间隔时间(秒)</span>
          <span class="form-tip">多块玻璃之间的物理间隔空隙,默认200mm</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -173,7 +173,7 @@
        >
          <el-input
            v-model="item.key"
            placeholder="位置代码(如900/901)"
            placeholder="位置代码(如1001/1002)"
            size="small"
            style="width: 150px; margin-right: 10px;"
            @change="handlePositionKeyChange(index)"
@@ -221,7 +221,7 @@
const config = ref({
  vehicleCapacity: 6000,
  vehicleSpeed: 1.0,
  glassIntervalMs: 1000,
  glassGap: 200,
  defaultGlassLength: 2000,
  homePosition: 0,
  minRange: 1,
@@ -270,7 +270,6 @@
syncPositionListFromConfig()
// 时间字段(秒)- 用于前端显示和输入
const glassIntervalSeconds = ref(1.0)
const idleMonitorIntervalSeconds = ref(2.0)
const taskMonitorIntervalSeconds = ref(1.0)
const mesConfirmTimeoutSeconds = ref(30)
@@ -282,7 +281,7 @@
    config.value = {
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      vehicleSpeed: newVal.vehicleSpeed ?? 1.0,
      glassIntervalMs: newVal.glassIntervalMs ?? 1000,
      glassGap: newVal.glassGap ?? 200,
      defaultGlassLength: newVal.defaultGlassLength ?? 2000,
      homePosition: newVal.homePosition ?? 0,
      minRange: newVal.minRange ?? 1,
@@ -295,7 +294,6 @@
      positionMapping: newVal.positionMapping || {}
    }
    // 将毫秒转换为秒用于显示
    glassIntervalSeconds.value = (config.value.glassIntervalMs ?? 1000) / 1000
    idleMonitorIntervalSeconds.value = (config.value.idleMonitorIntervalMs ?? 2000) / 1000
    taskMonitorIntervalSeconds.value = (config.value.taskMonitorIntervalMs ?? 1000) / 1000
    mesConfirmTimeoutSeconds.value = (config.value.mesConfirmTimeoutMs ?? 30000) / 1000
@@ -305,11 +303,6 @@
}, { immediate: true, deep: true })
// 监听秒字段变化,转换为毫秒并更新config
watch(glassIntervalSeconds, (val) => {
  config.value.glassIntervalMs = Math.round(val * 1000)
  emitConfigUpdate()
})
watch(idleMonitorIntervalSeconds, (val) => {
  config.value.idleMonitorIntervalMs = Math.round(val * 1000)
  emitConfigUpdate()
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
@@ -2,18 +2,6 @@
  <div class="workstation-transfer-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="扫码间隔(秒)">
          <el-input-number
            v-model="scanIntervalSeconds"
            :min="1"
            :max="60"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">定时查询最近扫码玻璃的时间间隔,默认10秒</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="缓冲判定时间(秒)">
          <el-input-number
            v-model="transferDelaySeconds"
@@ -40,6 +28,21 @@
          <span class="form-tip">可装载的最大宽度(毫米),默认6000mm</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="玻璃间隙(mm)">
          <el-input-number
            v-model="config.glassGap"
            :min="0"
            :max="1000"
            :step="10"
            style="width: 100%;"
          />
          <span class="form-tip">多块玻璃之间的物理间隔空隙,默认200mm</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="监控间隔(秒)">
          <el-input-number
@@ -68,7 +71,7 @@
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="位置值(格)">
        <el-form-item label="卧转立编号">
          <el-input-number
            v-model="config.inPosition"
            :min="0"
@@ -76,7 +79,7 @@
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">写入PLC的inPosition值(格子)</span>
          <span class="form-tip">写入PLC的inPosition字段,表示卧转立编号</span>
        </el-form-item>
      </el-col>
    </el-row>
@@ -97,16 +100,15 @@
// 配置数据
const config = ref({
  scanIntervalMs: 10000,
  transferDelayMs: 30000,
  vehicleCapacity: 6000,
  glassGap: 200,
  monitorIntervalMs: 10000,
  workLine: null,
  inPosition: null
})
// 时间字段(秒)- 用于前端显示和输入
const scanIntervalSeconds = ref(10)
const transferDelaySeconds = ref(30)
const monitorIntervalSeconds = ref(10)
@@ -114,31 +116,20 @@
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      scanIntervalMs: newVal.scanIntervalMs ?? 10000,
      transferDelayMs: newVal.transferDelayMs ?? 30000,
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      monitorIntervalMs: newVal.monitorIntervalMs ?? newVal.scanIntervalMs ?? 10000,
      glassGap: newVal.glassGap ?? 200,
      monitorIntervalMs: newVal.monitorIntervalMs ?? 10000,
      workLine: newVal.workLine ?? null,
      inPosition: newVal.inPosition ?? null
    }
    // 将毫秒转换为秒用于显示
    scanIntervalSeconds.value = (config.value.scanIntervalMs ?? 10000) / 1000
    transferDelaySeconds.value = (config.value.transferDelayMs ?? 30000) / 1000
    monitorIntervalSeconds.value = (config.value.monitorIntervalMs ?? 10000) / 1000
  }
}, { immediate: true, deep: true })
// 监听秒字段变化,转换为毫秒并更新config
watch(scanIntervalSeconds, (val) => {
  config.value.scanIntervalMs = Math.round(val * 1000)
  // 如果monitorIntervalMs未设置,则使用scanIntervalMs
  if (!props.modelValue?.monitorIntervalMs) {
    config.value.monitorIntervalMs = config.value.scanIntervalMs
    monitorIntervalSeconds.value = val
  }
  emit('update:modelValue', { ...config.value })
})
watch(transferDelaySeconds, (val) => {
  config.value.transferDelayMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
@@ -152,6 +143,7 @@
// 监听config其他字段变化,同步到父组件
watch(() => [
  config.value.vehicleCapacity,
  config.value.glassGap,
  config.value.workLine,
  config.value.inPosition
], () => {
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
@@ -3,11 +3,6 @@
    <div class="main-grid">
      <div class="left-panel">
        <GroupList @select="handleGroupSelect" />
        <GroupTopology
          v-if="selectedGroup"
          :group="selectedGroup"
          class="topology-panel"
        />
      </div>
      <div class="right-panel">
        <el-tabs v-model="activeTab" type="card" class="workbench-tabs">
@@ -42,7 +37,6 @@
<script setup>
import { computed, ref, watch } from 'vue'
import GroupList from './components/DeviceGroup/GroupList.vue'
import GroupTopology from './components/DeviceGroup/GroupTopology.vue'
import TaskOrchestration from './components/MultiDeviceTest/TaskOrchestration.vue'
import ExecutionMonitor from './components/MultiDeviceTest/ExecutionMonitor.vue'
import ResultAnalysis from './components/MultiDeviceTest/ResultAnalysis.vue'
@@ -118,11 +112,6 @@
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.topology-panel {
  flex: 1;
  min-height: 300px;
}
.right-panel {
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue
@@ -44,9 +44,19 @@
                <div class="node-name">{{ device.deviceName || device.deviceCode }}</div>
                <div class="node-type">{{ getDeviceTypeLabel(device.deviceType) }}</div>
                <div class="node-status">
                  <el-tag :type="getStatusType(device.status)" size="small">
                    {{ getStatusLabel(device.status) }}
                  <el-tag :type="getStatusType(getDeviceStatus(device))" size="small">
                    {{ getStatusLabel(getDeviceStatus(device)) }}
                  </el-tag>
                </div>
                <div class="node-actions">
                  <el-button
                    size="small"
                    text
                    @click.stop="clearPlc(device)"
                    :loading="clearingDeviceId === (device.deviceId || device.id)"
                  >
                    清空 PLC
                  </el-button>
                </div>
              </div>
            </div>
@@ -91,9 +101,28 @@
          {{ getDeviceTypeLabel(selectedDevice.deviceType) }}
        </el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="getStatusType(selectedDevice.status)">
            {{ getStatusLabel(selectedDevice.status) }}
          </el-tag>
          <div class="status-control">
            <el-tag :type="getStatusType(getDeviceStatus(selectedDevice))">
              {{ getStatusLabel(getDeviceStatus(selectedDevice)) }}
            </el-tag>
            <el-switch
              v-if="isLoadVehicleDevice(selectedDevice)"
              :model-value="Boolean(selectedDevice.onlineState)"
              active-text="联机"
              inactive-text="脱机"
              :loading="togglingDeviceId === (selectedDevice.deviceId || selectedDevice.id)"
              size="small"
              @change="(val) => toggleOnlineState(selectedDevice, val)"
            />
            <el-button
              size="small"
              text
              @click="clearPlc(selectedDevice)"
              :loading="clearingDeviceId === (selectedDevice.deviceId || selectedDevice.id)"
            >
              清空 PLC
            </el-button>
          </div>
        </el-descriptions-item>
        <el-descriptions-item label="PLC IP" v-if="selectedDevice.plcIp">
          {{ selectedDevice.plcIp }}
@@ -115,7 +144,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
  Refresh,
@@ -127,7 +156,7 @@
  Box,
  Folder
} from '@element-plus/icons-vue'
import { deviceGroupApi } from '@/api/device/deviceManagement'
import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement'
const props = defineProps({
  group: {
@@ -140,6 +169,10 @@
const devices = ref([])
const layoutMode = ref('horizontal') // 'horizontal' | 'vertical'
const selectedDevice = ref(null)
const togglingDeviceId = ref(null)
const clearingDeviceId = ref(null)
const refreshIntervalMs = 5000
let refreshTimer = null
const fetchDevices = async () => {
  if (!props.group) {
@@ -163,11 +196,14 @@
      ? rawList.data
      : []
    // 按执行顺序排序
    devices.value = deviceList.sort((a, b) => {
      const orderA = a.executionOrder || a.order || 0
      const orderB = b.executionOrder || b.order || 0
      return orderA - orderB
    })
    devices.value = deviceList
      .map((device) => normalizeDevice(device))
      .sort((a, b) => {
        const orderA = a.executionOrder || a.order || 0
        const orderB = b.executionOrder || b.order || 0
        return orderA - orderB
      })
    syncSelectedDevice()
  } catch (error) {
    ElMessage.error(error?.message || '加载设备列表失败')
    devices.value = []
@@ -178,6 +214,21 @@
const handleRefresh = () => {
  fetchDevices()
}
const stopAutoRefresh = () => {
  if (refreshTimer) {
    clearInterval(refreshTimer)
    refreshTimer = null
  }
}
const startAutoRefresh = () => {
  stopAutoRefresh()
  if (!props.group) return
  refreshTimer = setInterval(() => {
    fetchDevices()
  }, refreshIntervalMs)
}
const toggleLayout = () => {
@@ -250,13 +301,129 @@
  () => {
    fetchDevices()
    selectedDevice.value = null
    startAutoRefresh()
  },
  { immediate: true }
)
onMounted(() => {
  startAutoRefresh()
})
onUnmounted(() => {
  stopAutoRefresh()
})
// 点击节点选择设备
const handleNodeClick = (device) => {
  selectedDevice.value = device
}
const syncSelectedDevice = () => {
  if (!selectedDevice.value) return
  const deviceId = selectedDevice.value.deviceId || selectedDevice.value.id
  if (!deviceId) return
  const updated = devices.value.find(
    (item) => (item.deviceId || item.id) === deviceId
  )
  if (updated) {
    selectedDevice.value = updated
  }
}
const isLoadVehicleDevice = (device) => {
  if (!device || !device.deviceType) return false
  const type = device.deviceType.toUpperCase()
  return type.includes('VEHICLE') || type.includes('大车')
}
const normalizeDevice = (device) => {
  if (!device) return device
  const normalized = { ...device }
  if (normalized.onlineState !== undefined) {
    normalized.onlineState = toBoolean(normalized.onlineState)
  } else if (normalized.isOnline === true || normalized.isOnline === false) {
    normalized.onlineState = normalized.isOnline
  } else if (normalized.status) {
    normalized.onlineState = String(normalized.status).toUpperCase() === 'ONLINE'
  }
  if (isLoadVehicleDevice(normalized) && normalized.onlineState !== undefined) {
    normalized.status = normalized.onlineState ? 'ONLINE' : 'OFFLINE'
  }
  return normalized
}
const toBoolean = (value) => {
  if (value === true || value === false) return value
  if (typeof value === 'number') return value !== 0
  const str = String(value).trim().toLowerCase()
  if (str === 'true' || str === '1') return true
  if (str === 'false' || str === '0') return false
  return Boolean(value)
}
const getDeviceStatus = (device) => {
  if (!device) return 'UNKNOWN'
  if (isLoadVehicleDevice(device) && device.onlineState !== undefined) {
    return device.onlineState ? 'ONLINE' : 'OFFLINE'
  }
  if (device.isOnline === true || device.isOnline === false) {
    return device.isOnline ? 'ONLINE' : 'OFFLINE'
  }
  if (device.status) return device.status
  if (device.deviceStatus) return device.deviceStatus
  return 'UNKNOWN'
}
const toggleOnlineState = async (device, value) => {
  if (!device) return
  const deviceId = device.deviceId || device.id
  if (!deviceId) {
    ElMessage.warning('设备ID不存在,无法设置联机状态')
    return
  }
  try {
    togglingDeviceId.value = deviceId
    await deviceInteractionApi.executeOperation({
      deviceId,
      operation: 'setOnlineState',
      params: {
        onlineState: value
      }
    })
    device.onlineState = value
    device.status = value ? 'ONLINE' : 'OFFLINE'
    if (selectedDevice.value && (selectedDevice.value.deviceId === deviceId || selectedDevice.value.id === deviceId)) {
      selectedDevice.value.onlineState = device.onlineState
      selectedDevice.value.status = device.status
    }
    ElMessage.success(`已将 ${device.deviceName || device.deviceCode} 设置为${value ? '联机' : '脱机'}`)
  } catch (error) {
    ElMessage.error(error?.message || '设置联机状态失败')
  } finally {
    togglingDeviceId.value = null
  }
}
const clearPlc = async (device) => {
  if (!device) return
  const deviceId = device.deviceId || device.id
  if (!deviceId) {
    ElMessage.warning('设备ID不存在,无法清空PLC')
    return
  }
  try {
    clearingDeviceId.value = deviceId
    await deviceInteractionApi.executeOperation({
      deviceId,
      operation: 'clearPlc'
    })
    ElMessage.success(`已清空 ${device.deviceName || device.deviceCode} 的PLC数据`)
  } catch (error) {
    ElMessage.error(error?.message || '清空PLC失败')
  } finally {
    clearingDeviceId.value = null
  }
}
defineExpose({
@@ -422,6 +589,12 @@
  justify-content: center;
}
.node-actions {
  margin-top: 6px;
  display: flex;
  justify-content: center;
}
.node-order {
  position: absolute;
  top: -8px;
@@ -447,6 +620,12 @@
  justify-content: center;
}
.status-control {
  display: flex;
  align-items: center;
  gap: 12px;
}
/* 水平布局:箭头在节点右侧 */
.topology-node-wrapper.layout-horizontal .node-arrow {
  margin-left: 20px;
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -128,6 +128,9 @@
          <div class="step-desc" v-if="step.retryCount > 0">
            重试次数:{{ step.retryCount }}
          </div>
          <div class="step-desc" v-if="step.successMessage">
            提示:{{ step.successMessage }}
          </div>
          <div class="step-desc error-message" v-if="step.errorMessage">
            <el-icon><Warning /></el-icon>
            错误:{{ step.errorMessage }}
@@ -367,6 +370,7 @@
      endTime: data.endTime ? new Date(data.endTime) : existingStep.endTime,
      durationMs: data.durationMs !== undefined ? data.durationMs : existingStep.durationMs,
      retryCount: data.retryCount !== undefined ? data.retryCount : existingStep.retryCount,
      successMessage: data.successMessage !== undefined ? data.successMessage : existingStep.successMessage,
      errorMessage: data.errorMessage || existingStep.errorMessage
    }
  } else if (data.stepOrder !== undefined) {
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -31,7 +31,7 @@
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="可选:如果输入玻璃ID,将使用输入的ID进行测试(代替卧转立扫码);如果不输入,将从数据库读取最近扫码的玻璃ID进行测试"
          placeholder="可选:输入玻璃ID,将使用输入的ID进行测试"
          show-word-limit
          :maxlength="5000"
        />
@@ -40,54 +40,14 @@
          <span v-else>未输入玻璃ID(正常模式:将从数据库读取最近扫码的玻璃ID)</span>
        </div>
      </el-form-item>
      <el-divider content-position="left">执行配置</el-divider>
      <el-form-item label="单片间隔 (秒)">
        <el-input-number
          v-model="form.glassIntervalSeconds"
          :min="0"
          :max="60"
          :step="0.1"
          :precision="1"
          placeholder="每个玻璃ID之间的间隔时间"
        />
        <div class="form-tip">多个玻璃ID时,每个玻璃ID传递之间的间隔时间(秒),用于模拟玻璃每片运动的时间。0表示一次性全部传递</div>
      </el-form-item>
      <el-form-item label="执行间隔 (ms)">
        <el-input-number
          v-model="form.executionInterval"
          :min="100"
          :max="10000"
          :step="100"
          placeholder="设备操作间隔时间"
        />
        <div class="form-tip">每个设备操作之间的间隔时间(毫秒)</div>
      </el-form-item>
      <el-form-item label="超时时间 (分钟)">
        <el-input-number
          v-model="form.timeoutMinutes"
          :min="1"
          :max="60"
          :step="1"
          placeholder="任务超时时间"
        />
        <div class="form-tip">任务执行的最大超时时间</div>
      </el-form-item>
      <el-form-item label="重试次数">
        <el-input-number
          v-model="form.retryCount"
          :min="0"
          :max="10"
          :step="1"
          placeholder="失败重试次数"
        />
        <div class="form-tip">设备操作失败时的最大重试次数</div>
      </el-form-item>
    </el-form>
    <!-- 设备组拓扑图 -->
    <GroupTopology
      v-if="group"
      :group="group"
      class="topology-section"
    />
  </div>
</template>
@@ -97,6 +57,7 @@
import { Delete, Promotion } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement'
import GroupTopology from '../DeviceGroup/GroupTopology.vue'
const props = defineProps({
  group: {
@@ -107,12 +68,7 @@
const emit = defineEmits(['task-started'])
//配置默认值
const form = reactive({
  glassIntervalSeconds: 10, // 单片间隔,默认10秒
  executionInterval: 1000,
  timeoutMinutes: 1,
  retryCount: 3
})
const form = reactive({})
const formRef = ref(null)
@@ -231,22 +187,8 @@
    
    // 构建任务参数
    // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取
    // 将秒转换为毫秒传给后端
    const glassIntervalMs = form.glassIntervalSeconds != null && form.glassIntervalSeconds !== undefined
      ? Math.round(form.glassIntervalSeconds * 1000)
      : 1000
    const parameters = {
      glassIds: glassIds.value.length > 0 ? glassIds.value : [],
      glassIntervalMs: glassIntervalMs,
      executionInterval: form.executionInterval || 1000
    }
    // 设备特定配置已移除,如有需要可在此扩展
    if (form.timeoutMinutes) {
      parameters.timeoutMinutes = form.timeoutMinutes
    }
    if (form.retryCount !== null) {
      parameters.retryCount = form.retryCount
      glassIds: glassIds.value.length > 0 ? glassIds.value : []
    }
    
    // 异步启动任务,立即返回,不阻塞
@@ -362,5 +304,9 @@
  margin-top: 4px;
  line-height: 1.4;
}
.topology-section {
  margin-top: 24px;
}
</style>