huang
16 小时以前 9c489617b002e71859597097c9d1d2f1b9fc0e56
完善modbus协议写入方法
14个文件已修改
645 ■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/plc/client/impl/ModbusPlcClient.java 351 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/plc/client/impl/S7PlcClient.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/plc/factory/PlcClientFactory.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java
@@ -1,5 +1,6 @@
package com.mes.device.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mes.device.entity.EngineeringSequence;
import com.mes.device.entity.GlassInfo;
import com.mes.device.service.EngineeringSequenceService;
@@ -10,6 +11,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.util.*;
@@ -116,7 +118,7 @@
            // 7. 返回MES的响应
            return ResponseEntity.status(mesResp.getStatusCode()).body(mesBody);
        } catch (org.springframework.web.client.ResourceAccessException e) {
        } catch (ResourceAccessException e) {
            // 连接超时或无法连接
            log.error("转发 MES 导入接口失败(连接问题) url={}, error={}", glassInfoService.getMesEngineeringImportUrl(), e.getMessage(), e);
            errorResponse.put("code", 500);
@@ -204,7 +206,7 @@
            
            // 2. 删除engineering_sequence表中的工程号记录(逻辑删除)
            EngineeringSequence sequence = engineeringSequenceService.getOne(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EngineeringSequence>()
                new LambdaQueryWrapper<EngineeringSequence>()
                    .eq(EngineeringSequence::getEngineeringId, engineeringId)
                    .eq(EngineeringSequence::getIsDeleted, 0) // 只查询未删除的记录
                    .last("LIMIT 1")
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -15,11 +15,12 @@
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static java.util.stream.IntStream.range;
/**
 * 玻璃信息服务实现类
@@ -242,8 +243,13 @@
        final String filmsIdDefaultFinal = filmsIdDefault;
        final double thicknessDefaultFinal = thicknessDefault;
        // 用于按 flowCardId 全局计数序号
        Map<String, Integer> flowCardSequenceCounter = new HashMap<>();
        // 生成日期字符串(yyMMdd格式),用于流程卡ID生成
        LocalDate localDate = new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        String dateStr = localDate.format(DateTimeFormatter.ofPattern("yyMMdd"));
        // 用于存储每个玻璃ID对应的流程卡ID(同一玻璃ID的多个玻璃共享同一个流程卡ID)
        Map<String, String> glassIdFlowCardIdMap = new HashMap<>();
        // 用于按 flowCardId 计数 temperingFeedSequence
        Map<String, Integer> temperingFeedSequenceCounter = new HashMap<>();
        // 用于按 flowCardId 分配 temperingLayoutId
@@ -263,90 +269,112 @@
            }
        }
        List<Map<String, Object>> glassInfolList = excelRows.stream()
                .flatMap(row -> {
                    Object qtyObj = row.getOrDefault("quantity", 1);
                    int qty = parseDouble(qtyObj, 1) > 0 ? (int) parseDouble(qtyObj, 1) : 1;
        List<Map<String, Object>> glassInfolList = new ArrayList<>();
        for (Map<String, Object> row : excelRows) {
            Object qtyObj = row.getOrDefault("quantity", 1);
            int qty = parseDouble(qtyObj, 1) > 0 ? (int) parseDouble(qtyObj, 1) : 1;
                    String glassId = str(row.get("glassId"));
                    String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
                    String flowCardId = str(row.get("flowCardId"));
                    // orderNumber 是整型(玻璃类型),从 Excel 读取或使用默认值 1
                    Object orderNumberObj = row.get("orderNumber");
                    final Integer finalOrderNumber = orderNumberObj != null
                            ? (int) parseDouble(orderNumberObj, 1)
                            : 1;
                    String productName = str(row.get("productName"));
                    String customerName = str(row.get("customerName"));
                    double width = parseDouble(row.get("width"), 0d);
                    double height = parseDouble(row.get("length"), 0d);
                    double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
                    // 计算 rawSequence
                    String rawKey = width + "_" + height + "_" + thickness + "_" + filmsId;
                    Integer rawSequence = rawSequenceMap.get(rawKey);
            String glassId = str(row.get("glassId"));
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            String flowCardId = str(row.get("flowCardId"));
            // 如果流程卡ID为空,按新规则生成:NG + yyMMdd + 序号(两位,使用玻璃ID) + A001
            if (flowCardId.isEmpty()) {
                // 检查是否已为该玻璃ID生成过流程卡ID(同一玻璃ID的多个玻璃共享同一个流程卡ID)
                String generatedFlowCardId = glassIdFlowCardIdMap.get(glassId);
                if (generatedFlowCardId == null) {
                    // 使用玻璃ID作为序号(解析为整数,如果解析失败则使用1)
                    int sequence;
                    try {
                        sequence = Integer.parseInt(glassId.trim());
                        if (sequence <= 0) {
                            sequence = 1;
                        }
                    } catch (NumberFormatException e) {
                        log.warn("玻璃ID无法解析为整数,使用默认值1: glassId={}", glassId);
                        sequence = 1;
                    }
                    generatedFlowCardId = "NG" + dateStr + String.format("%02d", sequence) + "A001";
                    glassIdFlowCardIdMap.put(glassId, generatedFlowCardId);
                    log.info("为玻璃ID {} 生成流程卡ID: flowCardId={}", glassId, generatedFlowCardId);
                }
                flowCardId = generatedFlowCardId;
            }
            // 去掉尾部 "/数字"(如果有)
            String baseFlowCardId = flowCardId.replaceFirst("/\\d+$", "");
            // orderNumber 是整型(玻璃类型),从 Excel 读取或使用默认值 1
            Object orderNumberObj = row.get("orderNumber");
            final Integer finalOrderNumber = orderNumberObj != null
                    ? (int) parseDouble(orderNumberObj, 1)
                    : 1;
            String productName = str(row.get("productName"));
            String customerName = str(row.get("customerName"));
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("length"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            // 计算 rawSequence
            String rawKey = width + "_" + height + "_" + thickness + "_" + filmsId;
            Integer rawSequence = rawSequenceMap.get(rawKey);
                    int finalQty = qty;
                    log.info("解析到数量:row={}, quantity={}, 最终qty={}", row, qtyObj, finalQty);
                    return range(0, qty).mapToObj(idx -> {
                        String baseGlassId = engineerIdFinal + glassId;
                        String finalGlassId = finalQty > 1 ? baseGlassId + (idx + 1) : baseGlassId;
            int finalQty = qty;
            log.info("解析到数量:row={}, quantity={}, 最终qty={}", row, qtyObj, finalQty);
            // 按 flowCardId 分配 temperingLayoutId
            Integer temperingLayoutId = temperingLayoutIdMap.computeIfAbsent(baseFlowCardId, k -> nextTemperingLayoutId.getAndIncrement());
            // 为同一行的多个玻璃生成数据
            for (int idx = 0; idx < qty; idx++) {
                String baseGlassId = engineerIdFinal + glassId;
                String finalGlassId = finalQty > 1 ? baseGlassId + (idx + 1) : baseGlassId;
                // 按 flowCardId 递增 temperingFeedSequence
                int temperingFeedSequence = temperingFeedSequenceCounter.compute(baseFlowCardId, (k, v) -> (v == null ? 0 : v) + 1);
                String finalFlowCardSequence = baseFlowCardId + "/" + 1;
                log.debug("生成玻璃信息: glassId={}, idx={}, baseFlowCardId={}, finalFlowCardSequence={}, temperingLayoutId={}, temperingFeedSequence={}",
                        glassId, idx, baseFlowCardId, finalFlowCardSequence, temperingLayoutId, temperingFeedSequence);
                        String baseFlowCardId = flowCardId.isEmpty() ? baseGlassId : flowCardId;
                        // 如果 baseFlowCardId 已经包含尾部 "/数字",先去掉,再由后端统一追加序号
                        baseFlowCardId = baseFlowCardId.replaceFirst("/\\d+$", "");
                        // 按 flowCardId 全局递增序号
                        int sequenceNum = flowCardSequenceCounter.compute(baseFlowCardId, (k, v) -> (v == null ? 0 : v) + 1);
                        String finalFlowCardSequence = baseFlowCardId + "/" + 1;
                        // 按 flowCardId 分配 temperingLayoutId
                        Integer temperingLayoutId = temperingLayoutIdMap.computeIfAbsent(baseFlowCardId, k -> nextTemperingLayoutId.getAndIncrement());
                        // 按 flowCardId 递增 temperingFeedSequence
                        int temperingFeedSequence = temperingFeedSequenceCounter.compute(baseFlowCardId, (k, v) -> (v == null ? 0 : v) + 1);
                        log.debug("生成flowCardSequence: idx={}, baseFlowCardId={}, sequenceNum={}, finalFlowCardSequence={}, temperingLayoutId={}, temperingFeedSequence={}",
                                idx, baseFlowCardId, sequenceNum, finalFlowCardSequence, temperingLayoutId, temperingFeedSequence);
                        Map<String, Object> m = new HashMap<>();
                        m.put("xAxis", 0);
                        m.put("xCoordinate", 0);
                        m.put("yAxis", 0);
                        m.put("yCoordinate", 0);
                        m.put("glassId", finalGlassId);
                        m.put("engineerId", engineerIdFinal);
                        m.put("flowCardId", baseFlowCardId);
                        m.put("orderNumber", finalOrderNumber);
                        m.put("productSortNumber", 1); // 统一为1
                        m.put("hollowCombineDirection", "");
                        m.put("width", width);
                        m.put("height", height);
                        m.put("thickness", thickness);
                        m.put("filmsId", filmsId);
                        m.put("layer", 1);
                        m.put("totalLayer", 1);
                        m.put("edgWidth", width);
                        m.put("edgHeight", height);
                        m.put("isMultiple", finalQty > 1 ? 1 : 0); // 数量>1时为1
                        m.put("maxWidth", width);
                        m.put("maxHeight", height);
                        m.put("isHorizontal", 0);
                        m.put("rawSequence", rawSequence != null ? rawSequence : 0);
                        m.put("temperingLayoutId", temperingLayoutId);
                        m.put("temperingFeedSequence", temperingFeedSequence);
                        m.put("angle", 0);
                        m.put("ruleId", 0);
                        m.put("combine", 0);
                        m.put("markIcon", "");
                        m.put("filmRemove", 0);
                        m.put("flowCardSequence", finalFlowCardSequence);
                        m.put("process", "");
                        m.put("rawAngle", 0);
                        m.put("graphNo", 0);
                        m.put("processParam", "");
                        return m;
                    });
                })
                .collect(Collectors.toList());
                Map<String, Object> m = new HashMap<>();
                m.put("xAxis", 0);
                m.put("xCoordinate", 0);
                m.put("yAxis", 0);
                m.put("yCoordinate", 0);
                m.put("glassId", finalGlassId);
                m.put("engineerId", engineerIdFinal);
                m.put("flowCardId", baseFlowCardId);
                m.put("orderNumber", finalOrderNumber);
                m.put("productSortNumber", 1);
                m.put("hollowCombineDirection", "");
                m.put("width", width);
                m.put("height", height);
                m.put("thickness", thickness);
                m.put("filmsId", filmsId);
                m.put("layer", 1);
                m.put("totalLayer", 1);
                m.put("edgWidth", width);
                m.put("edgHeight", height);
                m.put("isMultiple", finalQty > 1 ? 1 : 0);
                m.put("maxWidth", width);
                m.put("maxHeight", height);
                m.put("isHorizontal", 0);
                m.put("rawSequence", rawSequence != null ? rawSequence : 0);
                m.put("temperingLayoutId", temperingLayoutId);
                m.put("temperingFeedSequence", temperingFeedSequence);
                m.put("angle", 0);
                m.put("ruleId", 0);
                m.put("combine", 0);
                m.put("markIcon", "");
                m.put("filmRemove", 0);
                m.put("flowCardSequence", finalFlowCardSequence);
                m.put("process", "");
                m.put("rawAngle", 0);
                m.put("graphNo", 0);
                m.put("processParam", "");
                glassInfolList.add(m);
            }
        }
        // 原片信息去重
        Map<String, Map<String, Object>> rawGlassMap = new HashMap<>();
@@ -377,7 +405,24 @@
            String glassId = str(row.get("glassId"));
            String flowCardId = str(row.get("flowCardId"));
            if (flowCardId.isEmpty()) {
                flowCardId = engineerIdFinal + glassId;
                // 使用已生成的流程卡ID(与glassInfolList中的逻辑保持一致)
                flowCardId = glassIdFlowCardIdMap.get(glassId);
                if (flowCardId == null) {
                    // 如果未生成,则按规则生成(理论上不应该走到这里,因为glassInfolList已经生成过)
                    int sequence;
                    try {
                        sequence = Integer.parseInt(glassId.trim());
                        if (sequence <= 0) {
                            sequence = 1;
                        }
                    } catch (NumberFormatException e) {
                        log.warn("玻璃ID无法解析为整数,使用默认值1: glassId={}", glassId);
                        sequence = 1;
                    }
                    flowCardId = "NG" + dateStr + String.format("%02d", sequence) + "A001";
                    glassIdFlowCardIdMap.put(glassId, flowCardId);
                    log.warn("流程卡ID未在glassInfolList中生成,此处补充生成: flowCardId={}, glassId={}", flowCardId, glassId);
                }
            }
            // 去掉尾部 "/数字"(如果有)
            flowCardId = flowCardId.replaceFirst("/\\d+$", "");
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java
@@ -9,6 +9,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
 * 设备逻辑处理器工厂类
@@ -83,7 +84,7 @@
     * 
     * @return 设备类型集合
     */
    public java.util.Set<String> getSupportedDeviceTypes() {
    public Set<String> getSupportedDeviceTypes() {
        return handlerMap.keySet();
    }
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -27,6 +27,7 @@
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
 * 大车设备逻辑处理器
@@ -1506,8 +1507,8 @@
            // 如果有多设备任务上下文,则记录本次MES下发的玻璃ID列表到上下文,供分批校验使用
            if (params != null) {
                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;
                if (ctxObj instanceof TaskExecutionContext) {
                    TaskExecutionContext ctx = (TaskExecutionContext) ctxObj;
                    List<String> batchIds = new ArrayList<>();
                    for (GlassTaskInfo g : glasses) {
                        if (g != null && g.glassId != null && !g.glassId.isEmpty()) {
@@ -1552,7 +1553,7 @@
            String taskType = isOutbound ? "出片" : "进片";
            String glassIds = glasses.stream()
                    .map(g -> g.glassId)
                    .collect(java.util.stream.Collectors.joining(","));
                    .collect(Collectors.joining(","));
            log.info("MES{}任务已创建: deviceId={}, glassCount={}, glassIds=[{}], 起始位置={}格, 目标位置={}格, 距离{}格->{}格, gotime={}ms({}秒), cartime={}ms({}秒)", 
                    taskType, deviceId, glasses.size(), glassIds,
                    firstGlass.startPosition, firstGlass.targetPosition,
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java
@@ -2,6 +2,8 @@
import lombok.Data;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
 * 车辆任务信息
@@ -49,10 +51,10 @@
    /**
     * 任务参数
     */
    private java.util.Map<String, Object> parameters;
    private Map<String, Object> parameters;
    
    public VehicleTask() {
        this.parameters = new java.util.HashMap<>();
        this.parameters = new HashMap<>();
    }
    
    /**
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
@@ -46,10 +46,10 @@
     * 默认实现:提示尚未实现具体逻辑
     */
    @Override
    protected DevicePlcVO.OperationResult doExecute(com.mes.device.entity.DeviceConfig deviceConfig,
    protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
                                                   String operation,
                                                   java.util.Map<String, Object> params,
                                                   java.util.Map<String, Object> logicParams) {
                                                   Map<String, Object> params,
                                                   Map<String, Object> logicParams) {
        log.warn("当前设备逻辑尚未实现: deviceType={}, operation={}", deviceConfig.getDeviceType(), operation);
        return DevicePlcVO.OperationResult.builder()
                .success(false)
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -15,6 +15,7 @@
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -176,7 +177,7 @@
        if (contextObj instanceof TaskExecutionContext) {
            TaskExecutionContext context = (TaskExecutionContext) contextObj;
            List<String> scannedGlassIds = (List<String>) context.getSharedData()
                    .computeIfAbsent("scannedGlassIds", k -> new java.util.ArrayList<>());
                    .computeIfAbsent("scannedGlassIds", k -> new ArrayList<>());
            if (!scannedGlassIds.contains(glassId)) {
                scannedGlassIds.add(glassId);
                log.debug("已保存扫描到的玻璃ID到共享数据: glassId={}", glassId);
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -11,6 +11,7 @@
import com.mes.interaction.workstation.config.WorkstationLogicConfig;
import com.mes.plc.client.PlcClient;
import com.mes.plc.factory.PlcClientFactory;
import com.mes.task.model.TaskExecutionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -192,16 +193,15 @@
            try {
                if (params != null) {
                    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;
                    if (ctxObj instanceof TaskExecutionContext) {
                        TaskExecutionContext ctx = (TaskExecutionContext) ctxObj;
                        List<String> batchGlassIds = batch.stream()
                                .map(GlassInfo::getGlassId)
                                .filter(Objects::nonNull)
                                .collect(Collectors.toList());
                        if (!batchGlassIds.isEmpty()) {
                            ctx.getSharedData().put("transferReadyGlassIds",
                                    new java.util.ArrayList<>(batchGlassIds));
                                    new ArrayList<>(batchGlassIds));
                            log.info("卧转立已输出批次玻璃到任务上下文: deviceId={}, glassIds={}",
                                    deviceConfig.getId(), batchGlassIds);
                        }
mes-processes/mes-plcSend/src/main/java/com/mes/plc/client/impl/ModbusPlcClient.java
@@ -1,14 +1,15 @@
package com.mes.plc.client.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.connect.modbus.ModbusTcpClient;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.util.ConfigJsonHelper;
import com.mes.plc.client.PlcClient;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
import java.util.*;
/**
 * Modbus协议PLC客户端实现
@@ -21,34 +22,34 @@
 */
@Slf4j
public class ModbusPlcClient implements PlcClient {
    // PLC IP地址
    private final String plcIp;
    // PLC端口
    private final int plcPort;
    // 从站地址
    private final int unitId;
    // 设备配置
    private final DeviceConfig device;
    // Modbus客户端实例
    private ModbusTcpClient modbusClient;
    // 连接状态
    private boolean connected = false;
    // 超时时间(毫秒)
    private int timeout = 5000;
    // ObjectMapper用于JSON解析
    private final ObjectMapper objectMapper = new ObjectMapper();
    // 地址映射缓存:字段名 -> Modbus地址
    private Map<String, String> addressMappingCache;
    /**
     * 构造函数
     *
@@ -58,7 +59,7 @@
        this.device = device;
        this.plcIp = device.getPlcIp();
        this.plcPort = device.getPlcPort() != null ? device.getPlcPort() : 502;
        // 从配置中获取从站地址,默认1
        int unitIdValue = 1;
        try {
@@ -75,8 +76,11 @@
            log.warn("解析unitId失败,使用默认值1: deviceId={}", device.getId(), e);
        }
        this.unitId = unitIdValue;
        // 初始化地址映射
        this.addressMappingCache = loadAddressMapping();
    }
    /**
     * 解析设备的extraParams
     *
@@ -87,27 +91,124 @@
        if (extraParamsJson == null || extraParamsJson.isEmpty()) {
            return new HashMap<>();
        }
        try {
            // 这里简化处理,实际项目中应该使用Jackson或Gson解析
            // 由于项目中已有ObjectMapper,这里暂时返回空Map,后续完善
            return new HashMap<>();
            TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {};
            return objectMapper.readValue(extraParamsJson, typeRef);
        } catch (Exception e) {
            log.error("解析extraParams失败: {}", extraParamsJson, e);
            return null;
            return new HashMap<>();
        }
    }
    /**
     * 加载地址映射配置(从configJson或extraParams.addressMapping)
     *
     * @return 字段名 -> Modbus地址的映射
     */
    private Map<String, String> loadAddressMapping() {
        Map<String, String> mapping = new HashMap<>();
        try {
            // 1. 优先从configJson获取
            Map<String, Object> configParams = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
            if (!configParams.isEmpty()) {
                for (Map.Entry<String, Object> entry : configParams.entrySet()) {
                    String fieldName = entry.getKey();
                    Object addressObj = entry.getValue();
                    if (addressObj != null) {
                        mapping.put(fieldName, String.valueOf(addressObj));
                    }
                }
                if (!mapping.isEmpty()) {
                    log.info("从configJson加载Modbus地址映射成功: deviceId={}, count={}", device.getId(), mapping.size());
                    return mapping;
                }
            }
            // 2. 从extraParams.addressMapping获取
            Map<String, Object> extraParams = parseExtraParams(device.getExtraParams());
            Object addressMappingObj = extraParams.get("addressMapping");
            if (addressMappingObj != null) {
                if (addressMappingObj instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> addrMap = (Map<String, Object>) addressMappingObj;
                    for (Map.Entry<String, Object> entry : addrMap.entrySet()) {
                        mapping.put(entry.getKey(), String.valueOf(entry.getValue()));
                    }
                } else if (addressMappingObj instanceof String) {
                    // 如果是JSON字符串,解析它
                    TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {};
                    Map<String, Object> addrMap = objectMapper.readValue((String) addressMappingObj, typeRef);
                    for (Map.Entry<String, Object> entry : addrMap.entrySet()) {
                        mapping.put(entry.getKey(), String.valueOf(entry.getValue()));
                    }
                }
                if (!mapping.isEmpty()) {
                    log.info("从extraParams.addressMapping加载Modbus地址映射成功: deviceId={}, count={}", device.getId(), mapping.size());
                    return mapping;
                }
            }
            log.warn("未找到Modbus地址映射配置: deviceId={}", device.getId());
        } catch (Exception e) {
            log.error("加载Modbus地址映射失败: deviceId={}", device.getId(), e);
        }
        return mapping;
    }
    /**
     * 获取字段对应的Modbus地址
     *
     * @param fieldName 字段名
     * @return Modbus地址(格式:功能码.寄存器地址,如 "3.40001")
     */
    private String getModbusAddress(String fieldName) {
        String address = addressMappingCache.get(fieldName);
        if (address == null || address.isEmpty()) {
            log.warn("字段 {} 未找到Modbus地址映射: deviceId={}", fieldName, device.getId());
            return null;
        }
        return address;
    }
    /**
     * 推断数据类型(根据字段名或值)
     */
    private String inferDataType(String fieldName, Object value) {
        if (value == null) {
            // 根据字段名推断
            String lowerName = fieldName.toLowerCase();
            if (lowerName.contains("float") || lowerName.contains("real")) {
                return "float";
            } else if (lowerName.contains("string") || lowerName.contains("str") || lowerName.contains("id")) {
                return "string";
            }
            return "int"; // 默认int
        }
        // 根据值类型推断
        if (value instanceof Float || value instanceof Double) {
            return "float";
        } else if (value instanceof String) {
            return "string";
        } else if (value instanceof Number) {
            return "int";
        }
        return "int";
    }
    @Override
    public boolean connect() {
        try {
            if (modbusClient != null && isConnected()) {
                return true;
            }
            // 创建Modbus客户端实例
            this.modbusClient = new ModbusTcpClient(this.plcIp, this.plcPort, this.unitId);
            // 连接PLC
            this.modbusClient.connect();
            this.connected = true;
@@ -119,7 +220,7 @@
            return false;
        }
    }
    @Override
    public void disconnect() {
        try {
@@ -134,46 +235,125 @@
            this.modbusClient = null;
        }
    }
    @Override
    public Map<String, Object> readAllData() {
        if (!isConnected() && !connect()) {
            log.error("Modbus PLC未连接,无法读取数据: {}:{}", this.plcIp, this.plcPort);
            return Collections.emptyMap();
        }
        try {
            // TODO: 实现Modbus读取所有数据
            // 这里暂时返回空Map,后续完善
            log.warn("Modbus readAllData未实现,返回空Map");
            return new HashMap<>();
            if (addressMappingCache.isEmpty()) {
                log.warn("Modbus地址映射为空,无法读取所有数据: deviceId={}", device.getId());
                return Collections.emptyMap();
            }
            // 读取所有配置的字段
            Map<String, Object> result = new HashMap<>();
            for (String fieldName : addressMappingCache.keySet()) {
                try {
                    Object value = readFieldValue(fieldName);
                    if (value != null) {
                        result.put(fieldName, value);
                    }
                } catch (Exception e) {
                    log.warn("读取字段失败: fieldName={}, deviceId={}, error={}", fieldName, device.getId(), e.getMessage());
                }
            }
            log.info("Modbus读取所有数据成功: deviceId={}, fieldCount={}", device.getId(), result.size());
            return result;
        } catch (Exception e) {
            log.error("Modbus PLC读取所有数据失败: {}:{}", this.plcIp, this.plcPort, e);
            this.connected = false;
            return Collections.emptyMap();
        }
    }
    @Override
    public Map<String, Object> readData(String... fields) {
        if (!isConnected() && !connect()) {
            log.error("Modbus PLC未连接,无法读取数据: {}:{}", this.plcIp, this.plcPort);
            return Collections.emptyMap();
        }
        if (fields == null || fields.length == 0) {
            return readAllData();
        }
        try {
            // TODO: 实现Modbus读取指定字段数据
            // 这里暂时返回空Map,后续完善
            log.warn("Modbus readData未实现,返回空Map");
            return new HashMap<>();
            Map<String, Object> result = new HashMap<>();
            for (String fieldName : fields) {
                if (fieldName == null || fieldName.isEmpty()) {
                    continue;
                }
                try {
                    Object value = readFieldValue(fieldName);
                    if (value != null) {
                        result.put(fieldName, value);
                    }
                } catch (Exception e) {
                    log.warn("读取字段失败: fieldName={}, deviceId={}, error={}", fieldName, device.getId(), e.getMessage());
                }
            }
            log.info("Modbus读取指定字段数据成功: deviceId={}, fields={}, resultCount={}",
                    device.getId(), Arrays.toString(fields), result.size());
            return result;
        } catch (Exception e) {
            log.error("Modbus PLC读取数据失败: {}:{}", this.plcIp, this.plcPort, e);
            this.connected = false;
            return Collections.emptyMap();
        }
    }
    /**
     * 读取单个字段的值
     */
    private Object readFieldValue(String fieldName) throws IOException {
        String address = getModbusAddress(fieldName);
        if (address == null) {
            return null;
        }
        // 根据字段名推断数据类型(优先从配置中获取)
        String dataType = inferDataType(fieldName, null);
        // 从extraParams中获取字段的数据类型配置
        Map<String, Object> extraParams = parseExtraParams(device.getExtraParams());
        @SuppressWarnings("unchecked")
        Map<String, Object> fieldConfigs = (Map<String, Object>) extraParams.get("fieldConfigs");
        if (fieldConfigs != null) {
            @SuppressWarnings("unchecked")
            Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName);
            if (fieldConfig != null && fieldConfig.get("dataType") != null) {
                dataType = String.valueOf(fieldConfig.get("dataType"));
            }
        }
        // 根据数据类型读取
        switch (dataType.toLowerCase()) {
            case "float":
            case "real":
                return modbusClient.readFloat(address);
            case "string":
            case "str":
                // 字符串需要指定长度,默认32字符
                int stringLength = 32;
                if (fieldConfigs != null) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName);
                    if (fieldConfig != null && fieldConfig.get("length") != null) {
                        stringLength = ((Number) fieldConfig.get("length")).intValue();
                    }
                }
                return modbusClient.readString(address, stringLength);
            case "int":
            case "integer":
            case "word":
            default:
                return modbusClient.readRegister(address);
        }
    }
@@ -183,21 +363,102 @@
            log.error("Modbus PLC未连接,无法写入数据: {}:{}", this.plcIp, this.plcPort);
            return false;
        }
        if (data == null || data.isEmpty()) {
            log.warn("写入数据为空,跳过操作: deviceId={}", device.getId());
            return true;
        }
        try {
            // TODO: 实现Modbus写入数据
            // 这里暂时返回true,后续完善
            log.warn("Modbus writeData未实现,返回成功");
            int successCount = 0;
            int failCount = 0;
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                String fieldName = entry.getKey();
                Object value = entry.getValue();
                if (value == null) {
                    continue; // 跳过null值
                }
                try {
                    writeFieldValue(fieldName, value);
                    successCount++;
                } catch (Exception e) {
                    log.error("写入字段失败: fieldName={}, value={}, deviceId={}, error={}",
                            fieldName, value, device.getId(), e.getMessage());
                    failCount++;
                }
            }
            if (failCount > 0) {
                log.warn("Modbus写入数据部分失败: deviceId={}, success={}, fail={}",
                        device.getId(), successCount, failCount);
                return false;
            }
            log.info("Modbus写入数据成功: deviceId={}, fieldCount={}", device.getId(), successCount);
            return true;
        } catch (Exception e) {
            log.error("Modbus PLC写入数据失败: {}:{}", this.plcIp, this.plcPort, e);
            this.connected = false;
            return false;
        }
    }
    /**
     * 写入单个字段的值
     */
    private void writeFieldValue(String fieldName, Object value) throws IOException {
        String address = getModbusAddress(fieldName);
        if (address == null) {
            throw new IllegalArgumentException("字段 " + fieldName + " 未找到Modbus地址映射");
        }
        // 根据值类型推断数据类型
        String dataType = inferDataType(fieldName, value);
        // 从extraParams中获取字段的数据类型配置
        Map<String, Object> extraParams = parseExtraParams(device.getExtraParams());
        @SuppressWarnings("unchecked")
        Map<String, Object> fieldConfigs = (Map<String, Object>) extraParams.get("fieldConfigs");
        if (fieldConfigs != null) {
            @SuppressWarnings("unchecked")
            Map<String, Object> fieldConfig = (Map<String, Object>) fieldConfigs.get(fieldName);
            if (fieldConfig != null && fieldConfig.get("dataType") != null) {
                dataType = String.valueOf(fieldConfig.get("dataType"));
            }
        }
        // 根据数据类型写入
        switch (dataType.toLowerCase()) {
            case "float":
            case "real":
                float floatValue;
                if (value instanceof Number) {
                    floatValue = ((Number) value).floatValue();
                } else {
                    floatValue = Float.parseFloat(String.valueOf(value));
                }
                modbusClient.writeFloat(address, floatValue);
                break;
            case "string":
            case "str":
                String stringValue = String.valueOf(value);
                modbusClient.writeString(address, stringValue);
                break;
            case "int":
            case "integer":
            case "word":
            default:
                int intValue;
                if (value instanceof Number) {
                    intValue = ((Number) value).intValue();
                } else {
                    intValue = Integer.parseInt(String.valueOf(value));
                }
                modbusClient.writeRegister(address, intValue);
                break;
        }
    }
@@ -214,17 +475,17 @@
            return false;
        }
    }
    @Override
    public String getPlcType() {
        return "MODBUS";
    }
    @Override
    public int getTimeout() {
        return this.timeout;
    }
    @Override
    public void setTimeout(int timeout) {
        this.timeout = timeout;
mes-processes/mes-plcSend/src/main/java/com/mes/plc/client/impl/S7PlcClient.java
@@ -9,6 +9,7 @@
import com.mes.s7.enhanced.EnhancedS7Serializer;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -70,12 +71,19 @@
    }
    
    static {
        // 初始化PLC类型映射
        // 初始化PLC类型映射(支持多种格式)
        // S7_1200 格式
        PLC_TYPE_MAP.put("S7_1200", EPlcType.S1200);
        PLC_TYPE_MAP.put("S7_1500", EPlcType.S1500);
        PLC_TYPE_MAP.put("S7_200", EPlcType.S200);
        PLC_TYPE_MAP.put("S7_300", EPlcType.S300);
        PLC_TYPE_MAP.put("S7_400", EPlcType.S400);
        // S1200 格式(不带下划线)
        PLC_TYPE_MAP.put("S1200", EPlcType.S1200);
        PLC_TYPE_MAP.put("S1500", EPlcType.S1500);
        PLC_TYPE_MAP.put("S200", EPlcType.S200);
        PLC_TYPE_MAP.put("S300", EPlcType.S300);
        PLC_TYPE_MAP.put("S400", EPlcType.S400);
    }
    
    /**
@@ -181,13 +189,13 @@
            }
            
            // 使用PlcDynamicDataService读取指定字段PLC数据
            Map<String, Object> data = this.plcDynamicDataService.readPlcData(this.device, java.util.Arrays.asList(fields), this.s7Serializer);
            Map<String, Object> data = this.plcDynamicDataService.readPlcData(this.device, Arrays.asList(fields), this.s7Serializer);
            if (data == null) {
                log.warn("PLC动态数据服务返回空数据: {}:{}", this.plcIp, this.plcPort);
                return new HashMap<>();
            }
            
            log.info("S7 PLC读取指定字段数据成功: {}:{}, fields={}", this.plcIp, this.plcPort, java.util.Arrays.toString(fields));
            log.info("S7 PLC读取指定字段数据成功: {}:{}, fields={}", this.plcIp, this.plcPort, Arrays.toString(fields));
            return data;
        } catch (Exception e) {
            log.error("S7 PLC读取数据失败: {}:{}", this.plcIp, this.plcPort, e);
mes-processes/mes-plcSend/src/main/java/com/mes/plc/factory/PlcClientFactory.java
@@ -118,10 +118,17 @@
        }
        
        // 根据PLC类型创建不同的客户端
        if (plcType.startsWith("S7")) {
        // 支持 S7_1200, S7_1500 等格式,也支持 S1200, S1500 等格式
        String normalizedPlcType = plcType.toUpperCase();
        if (normalizedPlcType.startsWith("S7") ||
            normalizedPlcType.startsWith("S1200") ||
            normalizedPlcType.startsWith("S1500") ||
            normalizedPlcType.startsWith("S200") ||
            normalizedPlcType.startsWith("S300") ||
            normalizedPlcType.startsWith("S400")) {
            // S7协议客户端
            return createS7Client(device);
        } else if (plcType.equals("MODBUS")) {
        } else if (normalizedPlcType.equals("MODBUS")) {
            // Modbus协议客户端
            return createModbusClient(device);
        } else {
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -34,6 +34,7 @@
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -540,7 +541,7 @@
            
            // 启动定时任务
            // 使用AtomicBoolean标记是否第一次执行
            final java.util.concurrent.atomic.AtomicBoolean firstExecution = new java.util.concurrent.atomic.AtomicBoolean(true);
            final AtomicBoolean firstExecution = new AtomicBoolean(true);
            
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java
@@ -5,6 +5,7 @@
import com.mes.task.entity.TaskStepDetail;
import com.mes.task.service.TaskStatusNotificationService;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -205,7 +206,7 @@
                    .data(createMessage("", data)));
            } catch (IOException e) {
                // 客户端断开连接是正常情况,使用DEBUG级别
                if (e instanceof org.apache.catalina.connector.ClientAbortException) {
                if (e instanceof ClientAbortException) {
                    log.debug("客户端断开SSE连接: taskId={}, event={}", taskId, eventName);
                } else {
                    log.warn("发送SSE消息失败: taskId={}, event={}", taskId, eventName, e);
@@ -234,7 +235,7 @@
                    .data(createMessage("", data)));
            } catch (IOException e) {
                // 客户端断开连接是正常情况,使用DEBUG级别
                if (e instanceof org.apache.catalina.connector.ClientAbortException) {
                if (e instanceof ClientAbortException) {
                    log.debug("客户端断开SSE连接: event={}", eventName);
                } else {
                    log.warn("发送SSE消息失败: event={}", eventName, e);
@@ -315,7 +316,7 @@
     */
    private String createMessage(String message, Map<String, Object> data) {
        try {
            Map<String, Object> result = new java.util.HashMap<>();
            Map<String, Object> result = new HashMap<>();
            result.put("timestamp", System.currentTimeMillis());
            if (message != null && !message.isEmpty()) {
                result.put("message", message);
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -578,6 +578,7 @@
    headerMap.quantity = 4
  }
  const padTwoZero = (num) => num.toString().padStart(2, '0')
  // 解析数据行
  const result = []
  for (let i = 1; i < jsonData.length; i++) {
@@ -616,7 +617,7 @@
    const qty = parseInt(quantity) || 1
    for (let j = 0; j < qty; j++) {
      // 如果数量大于1,为每条记录生成唯一的玻璃ID(追加序号)
      const finalGlassId = qty > 1 ? `${glassId}${j + 1}` : glassId
      const finalGlassId = qty > 1 ? `${glassId}${padTwoZero(j + 1)}` : glassId
      result.push({
        glassId: finalGlassId,