From 580aea4a0fa86a81c7efe7f009238010807ed6cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LAPTOP-S9HJSOEB=5C=E6=98=8A=E5=A4=A9?= Date: Mon, 27 Apr 2026 10:43:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- s7PlcConfig.txt | 16 + .../web/service/cron/S7MultiPlcService.java | 528 ++++++++++++++++++ 2 files changed, 544 insertions(+) create mode 100644 s7PlcConfig.txt create mode 100644 web/src/main/java/com/zhehekeji/web/service/cron/S7MultiPlcService.java diff --git a/s7PlcConfig.txt b/s7PlcConfig.txt new file mode 100644 index 0000000..357652c --- /dev/null +++ b/s7PlcConfig.txt @@ -0,0 +1,16 @@ +# S7 PLC 配置文件 +# 格式: ip, plcNumber, rack, slot, writeDataBlock, readDataBlock, +# palletName, palletDataType, palletOffset, +# batchName, batchDataType, batchOffset, +# dateName, dateDataType, dateOffset, +# snapName, snapDataType, snapOffset, +# photoName, photoDataType, photoOffset + +192.168.1.1,001,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.2,002,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.3,003,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.4,004,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.5,005,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.6,006,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.7,007,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 +192.168.1.8,008,0,1,DB221,DB222,盘点号,String[50],0.0,批次号,String[20],52.0,日期,String[20],74.0,允许拍照,byte,96.0,拍照结果,byte,98.0 diff --git a/web/src/main/java/com/zhehekeji/web/service/cron/S7MultiPlcService.java b/web/src/main/java/com/zhehekeji/web/service/cron/S7MultiPlcService.java new file mode 100644 index 0000000..ae1becf --- /dev/null +++ b/web/src/main/java/com/zhehekeji/web/service/cron/S7MultiPlcService.java @@ -0,0 +1,528 @@ +package com.zhehekeji.web.service.cron; + +import com.sourceforge.snap7.moka7.S7; +import com.sourceforge.snap7.moka7.S7Client; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.*; + +/** + * S7 多PLC模拟服务 + * 支持连接多个不同IP的PLC,读写分离 + */ +@Configuration +@Component +@EnableScheduling +@Slf4j +public class S7MultiPlcService { + + // PLC连接池映射: plcNumber -> 连接池 + private final Map> plcConnectionPools = new ConcurrentHashMap<>(); + + // PLC配置列表 + private final List plcConfigs = new CopyOnWriteArrayList<>(); + + // 地址偏移量映射: plcNumber+fieldName -> offset + private final Map offsetMap = new ConcurrentHashMap<>(); + + // 数据类型映射: plcNumber+fieldName -> dataType + private final Map dataTypeMap = new ConcurrentHashMap<>(); + + // 读取数据缓存: plcNumber -> 读取的数据 + private final Map readDataCache = new ConcurrentHashMap<>(); + + // 连接池大小 + private final int POOL_SIZE = 3; + + // 读取间隔(ms) + private static final int READ_INTERVAL = 1000; + + // 线程池 + private ExecutorService executorService; + + @PostConstruct + public void init() { + readPlcConfig(); + initConnectionPools(); + initExecutorService(); + log.info("S7多PLC服务初始化完成,共配置 {} 个PLC", plcConfigs.size()); + } + + /** + * PLC配置实体 + */ + @Data + public static class PlcConfig { + private String ip; + private String plcNumber; + private int rack; + private int slot; + private int writeDataBlock; // 如 DB221 -> 221 + private int readDataBlock; // 如 DB222 -> 222 + private String palletName; + private String palletDataType; + private double palletOffset; + private String batchName; + private String batchDataType; + private double batchOffset; + private String dateName; + private String dateDataType; + private double dateOffset; + private String snapName; + private String snapDataType; + private double snapOffset; + private String photoName; + private String photoDataType; + private double photoOffset; + } + + /** + * PLC读取数据 + */ + @Data + public static class PlcReadData { + private String palletNo; // 盘点号 + private String batchNo; // 批次号 + private String date; // 日期 + private byte snapFlag; // 允许拍照标志 + private byte photoResult; // 拍照结果 + private long updateTime; // 更新时间 + } + + /** + * 读取PLC配置文件 + */ + private void readPlcConfig() { + try (BufferedReader reader = new BufferedReader(new FileReader("./s7PlcConfig.txt"))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("#") || line.trim().isEmpty()) { + continue; + } + String[] parts = line.split(","); + if (parts.length >= 20) { + PlcConfig config = new PlcConfig(); + config.setIp(parts[0].trim()); + config.setPlcNumber(parts[1].trim()); + config.setRack(Integer.parseInt(parts[2].trim())); + config.setSlot(Integer.parseInt(parts[3].trim())); + config.setWriteDataBlock(Integer.parseInt(parts[4].trim().replace("DB", ""))); + config.setReadDataBlock(Integer.parseInt(parts[5].trim().replace("DB", ""))); + config.setPalletName(parts[6].trim()); + config.setPalletDataType(parts[7].trim()); + config.setPalletOffset(Double.parseDouble(parts[8].trim())); + config.setBatchName(parts[9].trim()); + config.setBatchDataType(parts[10].trim()); + config.setBatchOffset(Double.parseDouble(parts[11].trim())); + config.setDateName(parts[12].trim()); + config.setDateDataType(parts[13].trim()); + config.setDateOffset(Double.parseDouble(parts[14].trim())); + config.setSnapName(parts[15].trim()); + config.setSnapDataType(parts[16].trim()); + config.setSnapOffset(Double.parseDouble(parts[17].trim())); + config.setPhotoName(parts[18].trim()); + config.setPhotoDataType(parts[19].trim()); + config.setPhotoOffset(Double.parseDouble(parts[20].trim())); + + plcConfigs.add(config); + + // 注册映射 + String prefix = config.getPlcNumber(); + offsetMap.put(prefix + "_pallet", config.getPalletOffset()); + offsetMap.put(prefix + "_batch", config.getBatchOffset()); + offsetMap.put(prefix + "_date", config.getDateOffset()); + offsetMap.put(prefix + "_snap", config.getSnapOffset()); + offsetMap.put(prefix + "_photo", config.getPhotoOffset()); + + dataTypeMap.put(prefix + "_pallet", config.getPalletDataType()); + dataTypeMap.put(prefix + "_batch", config.getBatchDataType()); + dataTypeMap.put(prefix + "_date", config.getDateDataType()); + dataTypeMap.put(prefix + "_snap", config.getSnapDataType()); + dataTypeMap.put(prefix + "_photo", config.getPhotoDataType()); + } + } + } catch (IOException e) { + log.error("读取PLC配置文件失败", e); + throw new RuntimeException("读取PLC配置文件失败", e); + } + } + + /** + * 初始化所有PLC连接池 + */ + private void initConnectionPools() { + for (PlcConfig config : plcConfigs) { + BlockingQueue pool = new ArrayBlockingQueue<>(POOL_SIZE); + for (int i = 0; i < POOL_SIZE; i++) { + S7Client client = new S7Client(); + int result = client.ConnectTo(config.getIp(), config.getRack(), config.getSlot()); + if (result == 0) { + pool.offer(client); + log.info("PLC {} 连接成功: {}", config.getPlcNumber(), config.getIp()); + } else { + log.error("PLC {} 连接失败: {}, 错误码: {}", config.getPlcNumber(), config.getIp(), result); + } + } + plcConnectionPools.put(config.getPlcNumber(), pool); + } + } + + /** + * 初始化线程池 + */ + private void initExecutorService() { + executorService = new ThreadPoolExecutor( + plcConfigs.size(), + plcConfigs.size() * 2, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>() + ); + } + + /** + * 获取PLC连接 + */ + private S7Client getConnection(String plcNumber) { + BlockingQueue pool = plcConnectionPools.get(plcNumber); + if (pool == null) { + log.error("未找到PLC {} 的连接池", plcNumber); + return null; + } + try { + S7Client client = pool.take(); + if (!client.Connected) { + // 重新连接 + PlcConfig config = getPlcConfig(plcNumber); + if (config != null) { + client.ConnectTo(config.getIp(), config.getRack(), config.getSlot()); + } + } + return client; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("获取PLC {} 连接失败", plcNumber, e); + return null; + } + } + + /** + * 归还PLC连接 + */ + private void returnConnection(String plcNumber, S7Client client) { + BlockingQueue pool = plcConnectionPools.get(plcNumber); + if (pool != null && client != null) { + if (client.Connected) { + pool.offer(client); + } else { + // 重新创建连接 + PlcConfig config = getPlcConfig(plcNumber); + if (config != null) { + S7Client newClient = new S7Client(); + int result = newClient.ConnectTo(config.getIp(), config.getRack(), config.getSlot()); + if (result == 0) { + pool.offer(newClient); + } + } + } + } + } + + /** + * 获取PLC配置 + */ + private PlcConfig getPlcConfig(String plcNumber) { + return plcConfigs.stream() + .filter(c -> c.getPlcNumber().equals(plcNumber)) + .findFirst() + .orElse(null); + } + + /** + * 解析数据类型对应的字节长度 + */ + private int getDataTypeSize(String dataType) { + if (dataType == null) return 0; + if (dataType.equals("byte")) return 1; + if (dataType.equals("String[20]")) return 20; + if (dataType.equals("String[50]")) return 50; + if (dataType.startsWith("String[")) { + String size = dataType.replace("String[", "").replace("]", ""); + return Integer.parseInt(size); + } + return 0; + } + + /** + * 从buffer中读取字符串 + */ + private String readString(byte[] buffer, int offset, int length) { + if (buffer == null || offset < 0 || offset + length > buffer.length) { + return ""; + } + // 查找字符串结束位置(通常以\0结尾或有长度前缀) + int end = offset; + for (int i = offset; i < offset + length && i < buffer.length; i++) { + if (buffer[i] == 0) { + end = i; + break; + } + end = i + 1; + } + return new String(buffer, offset, end - offset, StandardCharsets.ISO_8859_1).trim(); + } + + /** + * 读取指定PLC的所有数据(读取线程) + */ + public PlcReadData readPlcData(String plcNumber) { + PlcConfig config = getPlcConfig(plcNumber); + if (config == null) { + log.error("未找到PLC配置: {}", plcNumber); + return null; + } + + S7Client client = getConnection(plcNumber); + if (client == null) { + return readDataCache.get(plcNumber); + } + + try { + // 计算需要读取的总长度 + int maxOffset = (int) Math.max( + Math.max(config.getPalletOffset(), config.getBatchOffset()), + Math.max(config.getDateOffset(), config.getSnapOffset()) + ) + getDataTypeSize(config.getPalletDataType()); + + byte[] buffer = new byte[maxOffset + 50]; + int result = client.ReadArea(S7.S7AreaDB, config.getReadDataBlock(), 0, buffer.length, buffer); + + if (result != 0) { + log.error("PLC {} 读取失败,错误码: {}", plcNumber, result); + returnConnection(plcNumber, client); + return readDataCache.get(plcNumber); + } + + PlcReadData data = new PlcReadData(); + data.setPalletNo(readString(buffer, (int) config.getPalletOffset(), getDataTypeSize(config.getPalletDataType()))); + data.setBatchNo(readString(buffer, (int) config.getBatchOffset(), getDataTypeSize(config.getBatchDataType()))); + data.setDate(readString(buffer, (int) config.getDateOffset(), getDataTypeSize(config.getDateDataType()))); + data.setSnapFlag(buffer[(int) config.getSnapOffset()]); + data.setPhotoResult(buffer[(int) config.getPhotoOffset()]); + data.setUpdateTime(System.currentTimeMillis()); + + // 更新缓存 + readDataCache.put(plcNumber, data); + + log.debug("PLC {} 读取成功: 盘点号={}, 批次号={}, 日期={}, 拍照标志={}", + plcNumber, data.getPalletNo(), data.getBatchNo(), data.getDate(), data.getSnapFlag()); + + returnConnection(plcNumber, client); + return data; + + } catch (Exception e) { + log.error("PLC {} 读取异常", plcNumber, e); + returnConnection(plcNumber, client); + return readDataCache.get(plcNumber); + } + } + + /** + * 写入拍照结果到PLC(写入线程) + */ + public boolean writePhotoResult(String plcNumber, byte photoResult) { + PlcConfig config = getPlcConfig(plcNumber); + if (config == null) { + log.error("未找到PLC配置: {}", plcNumber); + return false; + } + + S7Client client = getConnection(plcNumber); + if (client == null) { + return false; + } + + try { + byte[] buffer = new byte[1]; + buffer[0] = photoResult; + + int result = client.WriteArea( + S7.S7AreaDB, + config.getWriteDataBlock(), + (int) config.getPhotoOffset(), + 1, + buffer + ); + + if (result == 0) { + log.info("PLC {} 写入拍照结果成功: {}", plcNumber, photoResult); + returnConnection(plcNumber, client); + return true; + } else { + log.error("PLC {} 写入拍照结果失败,错误码: {}", plcNumber, result); + returnConnection(plcNumber, client); + return false; + } + + } catch (Exception e) { + log.error("PLC {} 写入异常", plcNumber, e); + returnConnection(plcNumber, client); + return false; + } + } + + /** + * 写入byte类型数据 + */ + public boolean writeByteData(String plcNumber, String fieldName, byte value) { + PlcConfig config = getPlcConfig(plcNumber); + if (config == null) { + log.error("未找到PLC配置: {}", plcNumber); + return false; + } + + Double offset = offsetMap.get(plcNumber + "_" + fieldName); + if (offset == null) { + log.error("未找到字段偏移量: {}_{}", plcNumber, fieldName); + return false; + } + + S7Client client = getConnection(plcNumber); + if (client == null) { + return false; + } + + try { + byte[] buffer = new byte[1]; + buffer[0] = value; + + int result = client.WriteArea( + S7.S7AreaDB, + config.getWriteDataBlock(), + offset.intValue(), + 1, + buffer + ); + + if (result == 0) { + log.info("PLC {} 写入 {} 成功: {}", plcNumber, fieldName, value); + returnConnection(plcNumber, client); + return true; + } else { + log.error("PLC {} 写入 {} 失败,错误码: {}", plcNumber, fieldName, result); + returnConnection(plcNumber, client); + return false; + } + + } catch (Exception e) { + log.error("PLC {} 写入异常", plcNumber, e); + returnConnection(plcNumber, client); + return false; + } + } + + /** + * 获取所有PLC的最新读取数据 + */ + public Map getAllPlcReadData() { + return new HashMap<>(readDataCache); + } + + /** + * 获取指定PLC的最新读取数据 + */ + public PlcReadData getPlcReadData(String plcNumber) { + return readDataCache.get(plcNumber); + } + + /** + * 定时读取所有PLC数据(定时任务) + */ + @Scheduled(fixedDelay = READ_INTERVAL) + public void scheduledReadAllPlc() { + for (PlcConfig config : plcConfigs) { + final String plcNumber = config.getPlcNumber(); + executorService.submit(() -> { + try { + readPlcData(plcNumber); + } catch (Exception e) { + log.error("定时读取PLC {} 异常", plcNumber, e); + } + }); + } + } + + /** + * 清理无效连接(定时任务) + */ + @Scheduled(fixedRate = 60000) + public void cleanUpConnections() { + log.info("开始清理PLC连接池..."); + for (Map.Entry> entry : plcConnectionPools.entrySet()) { + String plcNumber = entry.getKey(); + BlockingQueue pool = entry.getValue(); + int cleaned = 0; + + List validClients = new ArrayList<>(); + S7Client[] clients = pool.toArray(new S7Client[0]); + + for (S7Client client : clients) { + pool.remove(client); + if (client != null && client.Connected) { + pool.offer(client); + validClients.add(client); + } else { + if (client != null) { + client.Disconnect(); + } + // 创建新连接 + PlcConfig config = getPlcConfig(plcNumber); + if (config != null) { + S7Client newClient = new S7Client(); + int result = newClient.ConnectTo(config.getIp(), config.getRack(), config.getSlot()); + if (result == 0) { + pool.offer(newClient); + validClients.add(newClient); + cleaned++; + } + } + } + } + + if (cleaned > 0) { + log.info("PLC {} 替换了 {} 个无效连接", plcNumber, cleaned); + } + } + log.info("PLC连接池清理完成"); + } + + /** + * 获取配置列表 + */ + public List getPlcConfigs() { + return new ArrayList<>(plcConfigs); + } + + /** + * 异步写入拍照结果 + */ + public void writePhotoResultAsync(String plcNumber, byte photoResult) { + executorService.submit(() -> writePhotoResult(plcNumber, photoResult)); + } + + /** + * 异步读取指定PLC数据 + */ + public void readPlcDataAsync(String plcNumber) { + executorService.submit(() -> readPlcData(plcNumber)); + } +}