| | |
| | | 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; |
| | |
| | | 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.*; |
| | |
| | | |
| | | // 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); |
| | |
| | | |
| | | // 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") |
| | |
| | | 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; |
| | | |
| | | /** |
| | | * 玻璃信息服务实现类 |
| | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | 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<>(); |
| | |
| | | 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+$", ""); |
| | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * 设备逻辑处理器工厂类 |
| | |
| | | * |
| | | * @return 设备类型集合 |
| | | */ |
| | | public java.util.Set<String> getSupportedDeviceTypes() { |
| | | public Set<String> getSupportedDeviceTypes() { |
| | | return handlerMap.keySet(); |
| | | } |
| | | |
| | |
| | | import javax.annotation.PreDestroy; |
| | | import java.util.*; |
| | | import java.util.concurrent.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * 大车设备逻辑处理器 |
| | |
| | | // 如果有多设备任务上下文,则记录本次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()) { |
| | |
| | | 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, |
| | |
| | | |
| | | import lombok.Data; |
| | | import java.time.LocalDateTime; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * 车辆任务信息 |
| | |
| | | /** |
| | | * 任务参数 |
| | | */ |
| | | private java.util.Map<String, Object> parameters; |
| | | private Map<String, Object> parameters; |
| | | |
| | | public VehicleTask() { |
| | | this.parameters = new java.util.HashMap<>(); |
| | | this.parameters = new HashMap<>(); |
| | | } |
| | | |
| | | /** |
| | |
| | | * 默认实现:提示尚未实现具体逻辑 |
| | | */ |
| | | @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) |
| | |
| | | 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; |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | |
| | | 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客户端实现 |
| | |
| | | */ |
| | | @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; |
| | | |
| | | |
| | | /** |
| | | * 构造函数 |
| | | * |
| | |
| | | this.device = device; |
| | | this.plcIp = device.getPlcIp(); |
| | | this.plcPort = device.getPlcPort() != null ? device.getPlcPort() : 502; |
| | | |
| | | |
| | | // 从配置中获取从站地址,默认1 |
| | | int unitIdValue = 1; |
| | | try { |
| | |
| | | log.warn("解析unitId失败,使用默认值1: deviceId={}", device.getId(), e); |
| | | } |
| | | this.unitId = unitIdValue; |
| | | |
| | | // 初始化地址映射 |
| | | this.addressMappingCache = loadAddressMapping(); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 解析设备的extraParams |
| | | * |
| | |
| | | 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; |
| | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void disconnect() { |
| | | try { |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | } |
| | | |
| | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public String getPlcType() { |
| | | return "MODBUS"; |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public int getTimeout() { |
| | | return this.timeout; |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void setTimeout(int timeout) { |
| | | this.timeout = timeout; |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | // 使用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); |
| | |
| | | } |
| | | |
| | | // 根据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 { |
| | |
| | | |
| | | import java.util.*; |
| | | import java.util.concurrent.*; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | |
| | | /** |
| | |
| | | |
| | | // 启动定时任务 |
| | | // 使用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 { |
| | |
| | | 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; |
| | | |
| | |
| | | .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); |
| | |
| | | .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); |
| | |
| | | */ |
| | | 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); |
| | |
| | | headerMap.quantity = 4 |
| | | } |
| | | |
| | | const padTwoZero = (num) => num.toString().padStart(2, '0') |
| | | // 解析数据行 |
| | | const result = [] |
| | | for (let i = 1; i < jsonData.length; i++) { |
| | |
| | | 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, |