feat:s7读

This commit is contained in:
2026-03-16 09:58:00 +08:00
parent 08fc36f850
commit 7b4fd4ff07
3 changed files with 312 additions and 285 deletions

View File

@@ -1,32 +1,37 @@
package org.nl.iot.core.driver.protocol.plcs7;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.plc4x.java.DefaultPlcDriverManager;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.messages.PlcReadRequest;
import org.apache.plc4x.java.api.messages.PlcReadResponse;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.api.value.PlcValue;
import org.nl.common.exception.CommonException;
import org.nl.iot.core.driver.bo.AttributeBO;
import org.nl.iot.core.driver.bo.DeviceBO;
import org.nl.iot.core.driver.bo.MetadataEventDTO;
import org.nl.iot.core.driver.bo.SiteBO;
import org.nl.iot.core.driver.entity.RValue;
import org.nl.iot.core.driver.entity.WResponse;
import org.nl.iot.core.driver.entity.WValue;
import org.nl.iot.core.driver.enums.AttributeTypeFlagEnum;
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
import org.nl.iot.core.driver.protocol.modbustcp.util.ModbusPlcValueConvertUtil;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.PlcS7PointVariable;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.S7Connector;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.S7Serializer;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.factory.S7ConnectorFactory;
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.factory.S7SerializerFactory;
import org.nl.iot.core.driver.protocol.plcs7.util.PlcS7Utils;
import org.nl.iot.core.driver.service.DriverCustomService;
import org.nl.iot.modular.iot.entity.IotConfig;
import org.nl.iot.modular.iot.entity.IotConnect;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
@@ -42,7 +47,7 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
* Plc Connector Map
* 仅供参考
*/
private Map<String, MyS7Connector> connectMap;
private Map<String, PlcConnection> connectMap;
@PostConstruct
@Override
@@ -67,299 +72,134 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
}
@Override
public RValue read(IotConnect connect, IotConfig config) {
/*
* PLC S7 数据读取逻辑
*
* 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。
* 该方法用于从 PLC S7 设备中读取指定点位的数据。
* 1. 获取设备的 S7 连接器。
* 2. 加锁以确保线程安全。
* 3. 使用 S7 序列化器读取点位数据。
* 4. 将读取到的数据封装为 RValue 对象返回。
* 5. 捕获并记录异常, 确保锁在 finally 块中释放。
*/
log.debug("Plc S7 Read, connect: {}, config: {}", connect, config);
MyS7Connector myS7Connector = getS7Connector(connect);
public RValue read(DeviceBO device, SiteBO point) {
log.debug("Plc S7 Read, connect: {}, config: {}", device, point);
PlcConnection myS7Connector = getS7Connector(device);
String tagAddress = "%" + point.getRegisterAddress() + ":" + point.getDataType();
log.info("构建读取请求 - 标签名: {}, 地址: {}", point.getAlias(), tagAddress);
PlcReadRequest readRequest = doBuildReadRequest(myS7Connector, Collections.singletonList(point)).build();
CompletableFuture<? extends PlcReadResponse> readFuture = readRequest.execute();
PlcReadResponse readResponse;
try {
myS7Connector.lock.writeLock().lock();
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
String type = config.getDataType();
PlcS7PointVariable plcs7PointVariable = getPointVariable(config, type);
return new RValue(config, connect, String.valueOf(serializer.dispense(plcs7PointVariable)));
readResponse = readFuture.get(10, TimeUnit.SECONDS);
if (readResponse.getResponseCode(point.getAlias()) != PlcResponseCode.OK) {
return new RValue(device, point, null, String.format(
"读取 S7 失败,设备编码:%s地址%s响应码%s"
, point.getAlias()
, point.getRegisterAddress()
, readResponse.getResponseCode(point.getAlias())
));
}
} catch (Exception e) {
log.error("Plc S7 Read Error: {}", e.getMessage());
return null;
} finally {
myS7Connector.lock.writeLock().unlock();
return new RValue(device, point, null, e.getMessage());
}
log.info("读取响应 - 可用标签: {}", readResponse.getTagNames());
return new RValue(device, point, ModbusPlcValueConvertUtil.convertPlcValueToString(readResponse.getPlcValue(point.getAlias())
, point.getDataType()), null);
}
@Override
public Boolean write(IotConnect device, IotConfig point, WValue wValue) {
log.debug("Plc S7 Write, device: {}, value: {}", device, wValue);
MyS7Connector myS7Connector = getS7Connector(device);
myS7Connector.lock.writeLock().lock();
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
PlcS7PointVariable plcs7PointVariable = getPointVariable(point, wValue.getType());
try {
store(serializer, plcs7PointVariable, wValue.getType(), wValue.getValue());
return true;
} catch (Exception e) {
log.error("Plc S7 Write Error: {}", e.getMessage());
return false;
} finally {
myS7Connector.lock.writeLock().unlock();
}
public List<RValue> batchRead(DeviceBO device, List<SiteBO> points) {
return batchReadValue(getS7Connector(device), device, points);
}
/**
* 向 PLC S7 写入数据
* <p>
* 该方法用于将指定类型的数据写入到 PLC S7 的指定点位。
* 1. 根据类型字符串获取对应的 {@link AttributeTypeFlagEnum} 枚举值。
* 2. 如果类型不支持, 抛出 {@link UnSupportException} 异常。
* 3. 根据类型将字符串值转换为相应的 Java 类型。
* 4. 使用 {@link S7Serializer} 将数据写入到 PLC S7 的指定数据块和字节偏移量位置。
* <p>
* 支持的数据类型包括:
* - INT: 整型
* - LONG: 长整型
* - FLOAT: 单精度浮点型
* - DOUBLE: 双精度浮点型
* - BOOLEAN: 布尔型
* - STRING: 字符串
*
* @param serializer S7 序列化器, 用于与 PLC S7 进行数据交互
* @param plcS7PointVariable PLC S7 点位变量信息, 包含数据块编号, 字节偏移量等
* @param type 数据类型字符串, 用于标识要写入的数据类型
* @param value 要写入的字符串形式的数据值
* @throws CommonException 如果数据类型不支持, 抛出此异常
*/
private void store(S7Serializer serializer, PlcS7PointVariable plcS7PointVariable, String type, String value) {
AttributeTypeFlagEnum valueType = AttributeTypeFlagEnum.ofCode(type);
if (Objects.isNull(valueType)) {
throw new CommonException("Unsupported type of " + type);
}
AttributeBO attributeBOConfig = new AttributeBO(value);
switch (valueType) {
case INT:
int intValue = attributeBOConfig.getValueByClass(Integer.class);
serializer.store(intValue, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
case LONG:
long longValue = attributeBOConfig.getValueByClass(Long.class);
serializer.store(longValue, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
case FLOAT:
float floatValue = attributeBOConfig.getValueByClass(Float.class);
serializer.store(floatValue, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
case DOUBLE:
double doubleValue = attributeBOConfig.getValueByClass(Double.class);
serializer.store(doubleValue, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
case BOOLEAN:
boolean booleanValue = attributeBOConfig.getValueByClass(Boolean.class);
serializer.store(booleanValue, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
case STRING:
serializer.store(value, plcS7PointVariable.getDbNum(), plcS7PointVariable.getByteOffset());
break;
default:
break;
}
@Override
public Boolean write(DeviceBO device, WValue wValue) {
return null;
}
@Override
public List<WResponse> batchWrite(DeviceBO device, List<WValue> wValue) {
return List.of();
}
/**
* 获取 PLC S7 连接器
* <p>
* 该方法用于从缓存中获取指定设备的 S7 连接器。如果缓存中不存在该设备的连接器,
* 则会根据驱动配置信息创建一个新的连接器, 并将其缓存以供后续使用。
* <p>
* 连接器创建过程中, 会从驱动配置中获取主机地址和端口号, 并初始化读写锁以确保线程安全。
* 如果连接器创建失败, 将抛出 {@link CommonException} 异常。
*
* @param connectId 设备ID, 用于标识唯一的设备连接器
* @param driverConfig 驱动配置信息, 包含连接 PLC 所需的主机地址和端口号等参数
* @return 返回与设备ID对应的 {@link MyS7Connector} 对象, 包含 S7 连接器和读写锁
* @throws CommonException 如果连接器创建失败, 抛出此异常
*/
private MyS7Connector getS7Connector(IotConnect connect) {
MyS7Connector myS7Connector = connectMap.get(connect.getId().toString());
if (Objects.isNull(myS7Connector)) {
myS7Connector = new MyS7Connector();
private PlcConnection getS7Connector(DeviceBO deviceBO) {
String deviceId = deviceBO.getId();
PlcConnection connection = connectMap.get(deviceId);
log.debug("Plc S7 Connection Info {}", connect);
try {
S7Connector s7Connector = S7ConnectorFactory.buildTCPConnector()
.withHost(connect.getHost())
.withPort(connect.getPort())
.build();
myS7Connector.setLock(new ReentrantReadWriteLock());
myS7Connector.setConnector(s7Connector);
} catch (Exception e) {
throw new CommonException("new s7connector fail" + e.getMessage());
}
connectMap.put(connect.getId().toString(), myS7Connector);
}
return myS7Connector;
}
/**
* 获取 PLC S7 点位变量信息
* <p>
* 该方法用于从点位配置中提取 PLC S7 点位变量信息, 并封装为 {@link PlcS7PointVariable} 对象。
* 点位配置中应包含以下关键属性:
* - dbNum: 数据块编号
* - byteOffset: 字节偏移量
* - bitOffset: 位偏移量
* - blockSize: 数据块大小
* - type: 点位数据类型
* <p>
* 如果点位配置中缺少上述任一属性, 将抛出 {@link NullPointerException} 异常。
*
* @param pointConfig 点位配置信息, 包含点位变量的相关属性
* @param type 点位数据类型, 用于标识点位数据的类型
* @return 返回封装好的 {@link PlcS7PointVariable} 对象, 包含点位变量的详细信息
* @throws NullPointerException 如果点位配置中缺少必要的属性, 抛出此异常
*/
private PlcS7PointVariable getPointVariable(IotConfig config, String type) {
log.debug("Plc S7 Point Attribute Config {}", config);
// DB 块地址解析
return parseS7Address(config.getRegisterAddress(), type);
}
// 通用解析方法支持DB格式DB1.DBX10.3和S7-200格式VB1、VD4、V1.3
public static PlcS7PointVariable parseS7Address(String address, String type) {
if (address == null || address.trim().isEmpty()) {
throw new IllegalArgumentException("S7地址不能为空");
}
String cleanAddress = address.trim().toUpperCase();
int dbNum = 0;
String area = "";
int byteOffset = -1;
int bitOffset = -1;
int size = -1;
// ========== 分支1解析S7-1200/1500的DB格式DB1.DBX10.3、DB2.DBD5 ==========
if (cleanAddress.startsWith("DB")) {
area = "DB";
// 拆分DB编号和地址主体DB1.DBX10.3 → DB1 和 DBX10.3
String[] dbAndAddress = cleanAddress.split("\\.DB", 2);
if (dbAndAddress.length != 2) {
throw new IllegalArgumentException("DB地址格式错误" + address);
}
// 解析DB编号
String dbNumStr = dbAndAddress[0].replace("DB", "");
try {
dbNum = Integer.parseInt(dbNumStr);
if (dbNum < 1) throw new IllegalArgumentException("DB编号必须≥1" + dbNumStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("DB编号不是有效数字" + dbNumStr, e);
}
// 解析类型和偏移DBX10.3 → X10.3
String addressBody = "DB" + dbAndAddress[1];
char typeSuffix = addressBody.charAt(2);
String offsetStr = addressBody.substring(3);
// 按类型解析
switch (typeSuffix) {
case 'X': // BOOL
String[] byteBit = offsetStr.split("\\.");
if (byteBit.length != 2) throw new IllegalArgumentException("BOOL地址需包含位偏移" + address);
byteOffset = parseIntCheck(byteBit[0], "字节偏移");
bitOffset = parseIntCheck(byteBit[1], "位偏移");
if (bitOffset < 0 || bitOffset > 7) throw new IllegalArgumentException("位偏移需0-7" + bitOffset);
size = 1;
break;
case 'B': // BYTE
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 1;
break;
case 'W': // WORD
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 2;
break;
case 'D': // DWORD/REAL
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 4;
break;
case 'R': // REAL
byteOffset = parseIntCheck(offsetStr, "字节偏移");
size = 4;
break;
default:
throw new IllegalArgumentException("不支持的DB类型后缀" + typeSuffix);
}
// ========== 分支2解析S7-200的VB/VD/V1.3格式 ==========
} else {
// 第一步判断是BOOL位V1.3、I0.1)还是字节/双字VB1、VD4
if (cleanAddress.contains(".")) {
// 处理BOOL位V1.3、I0.1、M2.5
String[] areaByteBit = cleanAddress.split("\\.");
if (areaByteBit.length != 2) throw new IllegalArgumentException("S7-200 BOOL地址格式错误" + address);
// 拆分存储区和字节偏移V1 → V 和 1
String areaByteStr = areaByteBit[0];
area = areaByteStr.substring(0, 1); // 取第一个字符V/I/Q/M/SM
String byteOffsetStr = areaByteStr.substring(1);
// 校验存储区合法性
if (!"VIQMSM".contains(area)) throw new IllegalArgumentException("不支持的S7-200存储区" + area);
// 解析字节和位偏移
byteOffset = parseIntCheck(byteOffsetStr, "字节偏移");
bitOffset = parseIntCheck(areaByteBit[1], "位偏移");
if (bitOffset < 0 || bitOffset > 7) throw new IllegalArgumentException("位偏移需0-7" + bitOffset);
size = 1; // BOOL占1字节
} else {
// 处理字节/双字VB1、VD4、IW2、MB3
// 拆分存储区+类型 和 地址VB1 → VB 和 1VD4 → VD 和 4
char typeSuffix = cleanAddress.charAt(1); // 第二个字符是类型B/W/D
area = cleanAddress.substring(0, 1); // 第一个字符是存储区V/I/Q/M/SM
String offsetStr = cleanAddress.substring(2);
// 校验存储区和类型
if (!"VIQMSM".contains(area)) throw new IllegalArgumentException("不支持的S7-200存储区" + area);
if (!"BWD".contains(String.valueOf(typeSuffix))) throw new IllegalArgumentException("不支持的S7-200类型后缀" + typeSuffix);
// 解析字节偏移和大小
byteOffset = parseIntCheck(offsetStr, "字节偏移");
switch (typeSuffix) {
case 'B': size = 1; break; // BYTE
case 'W': size = 2; break; // WORD
case 'D': size = 4; break; // DWORD/REAL
}
}
}
// 校验字节偏移合法性
if (byteOffset < 0) throw new IllegalArgumentException("字节偏移不能为负数:" + byteOffset);
return new PlcS7PointVariable(dbNum, byteOffset, bitOffset, size, type);
}
// 工具方法:解析数字并捕获异常
private static int parseIntCheck(String str, String fieldName) {
// 检查连接是否有效(核心修复:判断连接是否存在且未关闭)
try {
return Integer.parseInt(str);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(fieldName + "不是有效数字:" + str, e);
if (Objects.isNull(connection) || !connection.isConnected()) {
// 旧连接失效,先关闭再移除
if (Objects.nonNull(connection)) {
try {
connection.close();
} catch (Exception e) {
log.warn("关闭失效连接失败deviceId: {}", deviceId, e);
}
connectMap.remove(deviceId);
}
// 创建新连接
String connectionUrl = PlcS7Utils.getS7ConnectionUrl(deviceBO);
log.info("创建S7连接deviceId: {}, url: {}", deviceId, connectionUrl);
connection = new DefaultPlcDriverManager().getConnection(connectionUrl);
connectMap.put(deviceId, connection);
}
} catch (Exception e) {
log.error("创建S7连接失败deviceId: {}", deviceId, e);
throw new CommonException("PLC S7 连接失败:" + e.getMessage());
}
return connection;
}
public static PlcReadRequest.Builder doBuildReadRequest(PlcConnection s7Connection, List<SiteBO> points) {
PlcReadRequest.Builder readBuilder = s7Connection.readRequestBuilder();
for (SiteBO point : points) {
String tagAddress = "%" + point.getRegisterAddress() + ":" + point.getDataType();
// 构建读取请求
String tagName = point.getAlias();
// 校验地址格式
if (!ModbusPlcValueConvertUtil.containerType(point.getDataType())) {
log.warn("S7数据类型错误设备编码{}", tagName);
continue;
}
readBuilder.addTagAddress(tagName, tagAddress);
}
return readBuilder;
}
/**
* MyS7Connector 内部类
* <p>
* 该类用于封装与 PLC S7 连接相关的信息, 包括读写锁和 S7 连接器。
* 读写锁 {@link ReentrantReadWriteLock} 用于确保在多线程环境下对 S7 连接器的操作是线程安全的。
* S7 连接器 {@link S7Connector} 用于与 PLC S7 设备进行通信。
* <p>
* 该类提供了无参构造函数和全参构造函数, 并使用了 Lombok 注解自动生成 getter 和 setter 方法。
* 实现批量读取
* @param s7Connection
* @param deviceBO
* @param points
* @return
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
private static class MyS7Connector {
private ReentrantReadWriteLock lock;
private S7Connector connector;
@SneakyThrows
public List<RValue> batchReadValue(PlcConnection s7Connection, DeviceBO deviceBO, List<SiteBO> points) {
// 1. 解析配置
PlcReadRequest.Builder readBuilder = doBuildReadRequest(s7Connection, points);
// 3. 执行请求
PlcReadRequest readRequest = readBuilder.build();
CompletableFuture<? extends PlcReadResponse> readFuture = readRequest.execute();
PlcReadResponse readResponse = readFuture.get(10, TimeUnit.SECONDS);
List<RValue> list = new ArrayList<>();
// 4.组装数据
for (SiteBO point : points) {
try {
PlcResponseCode responseCode = readResponse.getResponseCode(point.getAlias());
// 4. 校验响应码
if (responseCode != PlcResponseCode.OK) {
list.add(new RValue(deviceBO, point, null, String.format(
"读取S7失败设备编码%s地址%s响应码%s", point.getAlias(), point.getRegisterAddress(), responseCode
)));
continue;
}
// 5. 取值并转换
PlcValue plcValue = readResponse.getPlcValue(point.getAlias());
list.add(new RValue(deviceBO, point, ModbusPlcValueConvertUtil.convertPlcValueToString(plcValue, point.getDataType()), ""));
} catch (Exception e) {
list.add(new RValue(deviceBO, point, null, String.format(
"读取S7失败设备编码%s地址%s响应码%s", point.getAlias(), point.getRegisterAddress(), e.getMessage()
)));
}
}
return list;
}
}

View File

@@ -0,0 +1,52 @@
package org.nl.iot.core.driver.protocol.plcs7.util;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import org.nl.iot.core.driver.bo.DeviceBO;
/**
* plc s7 工具类
* @author: lyd
* @date: 2026/3/12
*/
public class PlcS7Utils {
public static final String S7_CONN_PRX = "s7://";
public static String getS7ConnectionUrl(DeviceBO deviceBO) {
// 包含remote-rack、remote-slot、controller-type
JSONObject propertiesMap = JSONObject.parseObject(deviceBO.getProperties());
// 获取设备IP地址
String ip = deviceBO.getHost();
if (ip == null || ip.trim().isEmpty()) {
throw new IllegalArgumentException("设备IP地址不能为空");
}
String port = deviceBO.getPort().toString();
if (ObjectUtil.isEmpty(port.trim().isEmpty()) || port.equals("null")) {
port = "102";
}
// 获取连接参数,设置默认值
String remoteRack = propertiesMap.getString("remote-rack");
if (remoteRack == null || remoteRack.trim().isEmpty()) {
remoteRack = "0"; // 默认值
}
String remoteSlot = propertiesMap.getString("remote-slot");
if (remoteSlot == null || remoteSlot.trim().isEmpty()) {
remoteSlot = "1"; // 默认值
}
String controllerType = propertiesMap.getString("controller-type");
if (controllerType == null || controllerType.trim().isEmpty()) {
controllerType = "S7_1200"; // 默认值
}
// 构建S7连接URL
// return String.format("%s%s:%s?local-rack=0&local-slot=0&remote-rack=%s&remote-slot=%s&controller-type=%s",
// S7_CONN_PRX, ip, port, remoteRack, remoteSlot, controllerType);
// return String.format("%s%s:%s?local-rack=%s&local-slot=%s&controller-type=%s",
// S7_CONN_PRX, ip, port, remoteRack, remoteSlot, controllerType);
return S7_CONN_PRX + ip;
}
}