feat:s7读
This commit is contained in:
@@ -1,32 +1,37 @@
|
|||||||
package org.nl.iot.core.driver.protocol.plcs7;
|
package org.nl.iot.core.driver.protocol.plcs7;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.Resource;
|
import lombok.*;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.common.exception.CommonException;
|
||||||
import org.nl.iot.core.driver.bo.AttributeBO;
|
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.MetadataEventDTO;
|
||||||
|
import org.nl.iot.core.driver.bo.SiteBO;
|
||||||
import org.nl.iot.core.driver.entity.RValue;
|
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.entity.WValue;
|
||||||
import org.nl.iot.core.driver.enums.AttributeTypeFlagEnum;
|
import org.nl.iot.core.driver.enums.AttributeTypeFlagEnum;
|
||||||
import org.nl.iot.core.driver.enums.MetadataOperateTypeEnum;
|
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.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.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.S7Serializer;
|
||||||
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.factory.S7ConnectorFactory;
|
import org.nl.iot.core.driver.protocol.plcs7.util.PlcS7Utils;
|
||||||
import org.nl.iot.core.driver.protocol.plcs7.com.github.s7.api.factory.S7SerializerFactory;
|
|
||||||
import org.nl.iot.core.driver.service.DriverCustomService;
|
import org.nl.iot.core.driver.service.DriverCustomService;
|
||||||
import org.nl.iot.modular.iot.entity.IotConfig;
|
import org.nl.iot.modular.iot.entity.IotConfig;
|
||||||
import org.nl.iot.modular.iot.entity.IotConnect;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +47,7 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
|
|||||||
* Plc Connector Map
|
* Plc Connector Map
|
||||||
* 仅供参考
|
* 仅供参考
|
||||||
*/
|
*/
|
||||||
private Map<String, MyS7Connector> connectMap;
|
private Map<String, PlcConnection> connectMap;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Override
|
@Override
|
||||||
@@ -67,299 +72,134 @@ public class PlcS7ProtocolDriverImpl implements DriverCustomService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RValue read(IotConnect connect, IotConfig config) {
|
public RValue read(DeviceBO device, SiteBO point) {
|
||||||
/*
|
log.debug("Plc S7 Read, connect: {}, config: {}", device, point);
|
||||||
* PLC S7 数据读取逻辑
|
PlcConnection myS7Connector = getS7Connector(device);
|
||||||
*
|
String tagAddress = "%" + point.getRegisterAddress() + ":" + point.getDataType();
|
||||||
* 提示: 此处逻辑仅供参考, 请务必结合实际应用场景进行修改。
|
log.info("构建读取请求 - 标签名: {}, 地址: {}", point.getAlias(), tagAddress);
|
||||||
* 该方法用于从 PLC S7 设备中读取指定点位的数据。
|
|
||||||
* 1. 获取设备的 S7 连接器。
|
|
||||||
* 2. 加锁以确保线程安全。
|
|
||||||
* 3. 使用 S7 序列化器读取点位数据。
|
|
||||||
* 4. 将读取到的数据封装为 RValue 对象返回。
|
|
||||||
* 5. 捕获并记录异常, 确保锁在 finally 块中释放。
|
|
||||||
*/
|
|
||||||
log.debug("Plc S7 Read, connect: {}, config: {}", connect, config);
|
|
||||||
MyS7Connector myS7Connector = getS7Connector(connect);
|
|
||||||
|
|
||||||
|
PlcReadRequest readRequest = doBuildReadRequest(myS7Connector, Collections.singletonList(point)).build();
|
||||||
|
CompletableFuture<? extends PlcReadResponse> readFuture = readRequest.execute();
|
||||||
|
PlcReadResponse readResponse;
|
||||||
try {
|
try {
|
||||||
myS7Connector.lock.writeLock().lock();
|
readResponse = readFuture.get(10, TimeUnit.SECONDS);
|
||||||
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
|
if (readResponse.getResponseCode(point.getAlias()) != PlcResponseCode.OK) {
|
||||||
String type = config.getDataType();
|
return new RValue(device, point, null, String.format(
|
||||||
PlcS7PointVariable plcs7PointVariable = getPointVariable(config, type);
|
"读取 S7 失败,设备编码:%s,地址:%s,响应码:%s"
|
||||||
return new RValue(config, connect, String.valueOf(serializer.dispense(plcs7PointVariable)));
|
, point.getAlias()
|
||||||
} catch (Exception e) {
|
, point.getRegisterAddress()
|
||||||
log.error("Plc S7 Read Error: {}", e.getMessage());
|
, readResponse.getResponseCode(point.getAlias())
|
||||||
return null;
|
));
|
||||||
} finally {
|
|
||||||
myS7Connector.lock.writeLock().unlock();
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
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
|
@Override
|
||||||
public Boolean write(IotConnect device, IotConfig point, WValue wValue) {
|
public List<RValue> batchRead(DeviceBO device, List<SiteBO> points) {
|
||||||
log.debug("Plc S7 Write, device: {}, value: {}", device, wValue);
|
return batchReadValue(getS7Connector(device), device, points);
|
||||||
MyS7Connector myS7Connector = getS7Connector(device);
|
}
|
||||||
myS7Connector.lock.writeLock().lock();
|
|
||||||
S7Serializer serializer = S7SerializerFactory.buildSerializer(myS7Connector.getConnector());
|
|
||||||
PlcS7PointVariable plcs7PointVariable = getPointVariable(point, wValue.getType());
|
|
||||||
|
|
||||||
try {
|
@Override
|
||||||
store(serializer, plcs7PointVariable, wValue.getType(), wValue.getValue());
|
public Boolean write(DeviceBO device, WValue wValue) {
|
||||||
return true;
|
return null;
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Plc S7 Write Error: {}", e.getMessage());
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
myS7Connector.lock.writeLock().unlock();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 向 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) {
|
@Override
|
||||||
case INT:
|
public List<WResponse> batchWrite(DeviceBO device, List<WValue> wValue) {
|
||||||
int intValue = attributeBOConfig.getValueByClass(Integer.class);
|
return List.of();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 PLC S7 连接器
|
* 获取 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) {
|
private PlcConnection getS7Connector(DeviceBO deviceBO) {
|
||||||
MyS7Connector myS7Connector = connectMap.get(connect.getId().toString());
|
String deviceId = deviceBO.getId();
|
||||||
if (Objects.isNull(myS7Connector)) {
|
PlcConnection connection = connectMap.get(deviceId);
|
||||||
myS7Connector = new MyS7Connector();
|
|
||||||
|
|
||||||
log.debug("Plc S7 Connection Info {}", connect);
|
// 检查连接是否有效(核心修复:判断连接是否存在且未关闭)
|
||||||
try {
|
try {
|
||||||
S7Connector s7Connector = S7ConnectorFactory.buildTCPConnector()
|
if (Objects.isNull(connection) || !connection.isConnected()) {
|
||||||
.withHost(connect.getHost())
|
// 旧连接失效,先关闭再移除
|
||||||
.withPort(connect.getPort())
|
if (Objects.nonNull(connection)) {
|
||||||
.build();
|
try {
|
||||||
myS7Connector.setLock(new ReentrantReadWriteLock());
|
connection.close();
|
||||||
myS7Connector.setConnector(s7Connector);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new CommonException("new s7connector fail" + e.getMessage());
|
log.warn("关闭失效连接失败,deviceId: {}", deviceId, e);
|
||||||
}
|
}
|
||||||
connectMap.put(connect.getId().toString(), myS7Connector);
|
connectMap.remove(deviceId);
|
||||||
}
|
}
|
||||||
return myS7Connector;
|
// 创建新连接
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 PLC S7 点位变量信息
|
* 实现批量读取
|
||||||
* <p>
|
* @param s7Connection
|
||||||
* 该方法用于从点位配置中提取 PLC S7 点位变量信息, 并封装为 {@link PlcS7PointVariable} 对象。
|
* @param deviceBO
|
||||||
* 点位配置中应包含以下关键属性:
|
* @param points
|
||||||
* - dbNum: 数据块编号
|
* @return
|
||||||
* - byteOffset: 字节偏移量
|
|
||||||
* - bitOffset: 位偏移量
|
|
||||||
* - blockSize: 数据块大小
|
|
||||||
* - type: 点位数据类型
|
|
||||||
* <p>
|
|
||||||
* 如果点位配置中缺少上述任一属性, 将抛出 {@link NullPointerException} 异常。
|
|
||||||
*
|
|
||||||
* @param pointConfig 点位配置信息, 包含点位变量的相关属性
|
|
||||||
* @param type 点位数据类型, 用于标识点位数据的类型
|
|
||||||
* @return 返回封装好的 {@link PlcS7PointVariable} 对象, 包含点位变量的详细信息
|
|
||||||
* @throws NullPointerException 如果点位配置中缺少必要的属性, 抛出此异常
|
|
||||||
*/
|
*/
|
||||||
private PlcS7PointVariable getPointVariable(IotConfig config, String type) {
|
@SneakyThrows
|
||||||
log.debug("Plc S7 Point Attribute Config {}", config);
|
public List<RValue> batchReadValue(PlcConnection s7Connection, DeviceBO deviceBO, List<SiteBO> points) {
|
||||||
// DB 块地址解析
|
// 1. 解析配置
|
||||||
return parseS7Address(config.getRegisterAddress(), type);
|
PlcReadRequest.Builder readBuilder = doBuildReadRequest(s7Connection, points);
|
||||||
}
|
// 3. 执行请求
|
||||||
|
PlcReadRequest readRequest = readBuilder.build();
|
||||||
// 通用解析方法:支持DB格式(DB1.DBX10.3)和S7-200格式(VB1、VD4、V1.3)
|
CompletableFuture<? extends PlcReadResponse> readFuture = readRequest.execute();
|
||||||
public static PlcS7PointVariable parseS7Address(String address, String type) {
|
PlcReadResponse readResponse = readFuture.get(10, TimeUnit.SECONDS);
|
||||||
if (address == null || address.trim().isEmpty()) {
|
List<RValue> list = new ArrayList<>();
|
||||||
throw new IllegalArgumentException("S7地址不能为空!");
|
// 4.组装数据
|
||||||
}
|
for (SiteBO point : points) {
|
||||||
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 {
|
try {
|
||||||
dbNum = Integer.parseInt(dbNumStr);
|
PlcResponseCode responseCode = readResponse.getResponseCode(point.getAlias());
|
||||||
if (dbNum < 1) throw new IllegalArgumentException("DB编号必须≥1:" + dbNumStr);
|
// 4. 校验响应码
|
||||||
} catch (NumberFormatException e) {
|
if (responseCode != PlcResponseCode.OK) {
|
||||||
throw new IllegalArgumentException("DB编号不是有效数字:" + dbNumStr, e);
|
list.add(new RValue(deviceBO, point, null, String.format(
|
||||||
|
"读取S7失败,设备编码:%s,地址:%s,响应码:%s", point.getAlias(), point.getRegisterAddress(), responseCode
|
||||||
|
)));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// 解析类型和偏移(DBX10.3 → X10.3)
|
// 5. 取值并转换
|
||||||
String addressBody = "DB" + dbAndAddress[1];
|
PlcValue plcValue = readResponse.getPlcValue(point.getAlias());
|
||||||
char typeSuffix = addressBody.charAt(2);
|
list.add(new RValue(deviceBO, point, ModbusPlcValueConvertUtil.convertPlcValueToString(plcValue, point.getDataType()), ""));
|
||||||
String offsetStr = addressBody.substring(3);
|
} catch (Exception e) {
|
||||||
// 按类型解析
|
list.add(new RValue(deviceBO, point, null, String.format(
|
||||||
switch (typeSuffix) {
|
"读取S7失败,设备编码:%s,地址:%s,响应码:%s", point.getAlias(), point.getRegisterAddress(), e.getMessage()
|
||||||
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 和 1;VD4 → 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return list;
|
||||||
|
|
||||||
// 校验字节偏移合法性
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MyS7Connector 内部类
|
|
||||||
* <p>
|
|
||||||
* 该类用于封装与 PLC S7 连接相关的信息, 包括读写锁和 S7 连接器。
|
|
||||||
* 读写锁 {@link ReentrantReadWriteLock} 用于确保在多线程环境下对 S7 连接器的操作是线程安全的。
|
|
||||||
* S7 连接器 {@link S7Connector} 用于与 PLC S7 设备进行通信。
|
|
||||||
* <p>
|
|
||||||
* 该类提供了无参构造函数和全参构造函数, 并使用了 Lombok 注解自动生成 getter 和 setter 方法。
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
private static class MyS7Connector {
|
|
||||||
private ReentrantReadWriteLock lock;
|
|
||||||
private S7Connector connector;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.nl.iot.core.driver.entity.RValue;
|
|||||||
import org.nl.iot.core.driver.entity.WResponse;
|
import org.nl.iot.core.driver.entity.WResponse;
|
||||||
import org.nl.iot.core.driver.entity.WValue;
|
import org.nl.iot.core.driver.entity.WValue;
|
||||||
import org.nl.iot.core.driver.protocol.modbustcp.ModBusProtocolDriverImpl;
|
import org.nl.iot.core.driver.protocol.modbustcp.ModBusProtocolDriverImpl;
|
||||||
|
import org.nl.iot.core.driver.protocol.plcs7.PlcS7ProtocolDriverImpl;
|
||||||
import org.nl.iot.modular.iot.entity.IotConfig;
|
import org.nl.iot.modular.iot.entity.IotConfig;
|
||||||
import org.nl.iot.modular.iot.entity.IotConnect;
|
import org.nl.iot.modular.iot.entity.IotConnect;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -463,4 +464,138 @@ public class ApiTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PlcS7ProtocolDriverImpl plcS7ProtocolDriver;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void plcS7ReadTest() {
|
||||||
|
// 构建PLC S7连接对象
|
||||||
|
JSONObject properties = new JSONObject();
|
||||||
|
properties.put("remote-rack", "0");
|
||||||
|
properties.put("remote-slot", "0");
|
||||||
|
properties.put("controller-type", "S7_1200");
|
||||||
|
|
||||||
|
IotConnect connect = IotConnect.builder()
|
||||||
|
.id(2)
|
||||||
|
.code("PLC_S7_001")
|
||||||
|
.host("192.168.81.251")
|
||||||
|
.port(102)
|
||||||
|
.properties(JSONObject.toJSONString(properties))
|
||||||
|
.protocol("plc-s7")
|
||||||
|
.enabled(true)
|
||||||
|
.description("测试PLC S7连接")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建配置对象 - 读取DB1数据块的不同类型数据
|
||||||
|
IotConfig config1 = IotConfig.builder()
|
||||||
|
.id(1)
|
||||||
|
.connectId(2)
|
||||||
|
.alias("move")
|
||||||
|
.aliasName("DB1布尔值")
|
||||||
|
.registerAddress("DB1.DB0.0")
|
||||||
|
.dataType("WORD")
|
||||||
|
.readonly(true)
|
||||||
|
.enabled(true)
|
||||||
|
.description("测试DB1布尔值读取")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
IotConfig config2 = IotConfig.builder()
|
||||||
|
.id(2)
|
||||||
|
.connectId(2)
|
||||||
|
.alias("db1_int")
|
||||||
|
.aliasName("DB1整数值")
|
||||||
|
// .registerAddress("DB1.DB0.0")
|
||||||
|
.registerAddress("DB1.DBD0")
|
||||||
|
.dataType("DINT")
|
||||||
|
.readonly(true)
|
||||||
|
.enabled(true)
|
||||||
|
.description("测试DB1整数读取")
|
||||||
|
.build();
|
||||||
|
//
|
||||||
|
// IotConfig config3 = IotConfig.builder()
|
||||||
|
// .id(3)
|
||||||
|
// .connectId(2)
|
||||||
|
// .alias("db1_real")
|
||||||
|
// .aliasName("DB1实数值")
|
||||||
|
// .registerAddress("DB1.DBD4")
|
||||||
|
// .dataType("REAL")
|
||||||
|
// .readonly(true)
|
||||||
|
// .enabled(true)
|
||||||
|
// .description("测试DB1实数读取")
|
||||||
|
// .build();
|
||||||
|
|
||||||
|
// 执行读取操作
|
||||||
|
try {
|
||||||
|
System.out.println("========== 开始测试PLC S7读取 ==========");
|
||||||
|
System.out.println("连接信息: " + connect.getHost() + ":" + connect.getPort());
|
||||||
|
System.out.println("控制器类型: " + properties.getString("controller-type"));
|
||||||
|
System.out.println("机架号: " + properties.getString("remote-rack"));
|
||||||
|
System.out.println("插槽号: " + properties.getString("remote-slot"));
|
||||||
|
|
||||||
|
// 转换为DeviceBO
|
||||||
|
DeviceBO deviceBO = DeviceBO.builder()
|
||||||
|
.id(String.valueOf(connect.getId()))
|
||||||
|
.code(connect.getCode())
|
||||||
|
.properties(connect.getProperties())
|
||||||
|
.host(connect.getHost())
|
||||||
|
.port(connect.getPort())
|
||||||
|
.protocol(connect.getProtocol())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// // 测试读取布尔值
|
||||||
|
// System.out.println("\n--- 测试读取布尔值 ---");
|
||||||
|
// SiteBO siteBO1 = SiteBO.builder()
|
||||||
|
// .deviceCode(connect.getCode())
|
||||||
|
// .alias(config1.getAlias())
|
||||||
|
// .aliasName(config1.getAliasName())
|
||||||
|
// .registerAddress(config1.getRegisterAddress())
|
||||||
|
// .dataType(config1.getDataType())
|
||||||
|
// .readonly(config1.getReadonly())
|
||||||
|
// .build();
|
||||||
|
//
|
||||||
|
// RValue result1 = plcS7ProtocolDriver.read(deviceBO, siteBO1);
|
||||||
|
// System.out.println("地址: " + config1.getRegisterAddress());
|
||||||
|
// System.out.println("数据类型: " + config1.getDataType());
|
||||||
|
// System.out.println("读取结果: " + result1.getValue());
|
||||||
|
|
||||||
|
// 测试读取整数值
|
||||||
|
System.out.println("\n--- 测试读取整数值 ---");
|
||||||
|
SiteBO siteBO2 = SiteBO.builder()
|
||||||
|
.deviceCode(connect.getCode())
|
||||||
|
.alias(config2.getAlias())
|
||||||
|
.aliasName(config2.getAliasName())
|
||||||
|
.registerAddress(config2.getRegisterAddress())
|
||||||
|
.dataType(config2.getDataType())
|
||||||
|
.readonly(config2.getReadonly())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
RValue result2 = plcS7ProtocolDriver.read(deviceBO, siteBO2);
|
||||||
|
System.out.println("地址: " + config2.getRegisterAddress());
|
||||||
|
System.out.println("数据类型: " + config2.getDataType());
|
||||||
|
System.out.println("读取结果: " + result2.getValue());
|
||||||
|
|
||||||
|
// 测试读取实数值
|
||||||
|
// System.out.println("\n--- 测试读取实数值 ---");
|
||||||
|
// SiteBO siteBO3 = SiteBO.builder()
|
||||||
|
// .deviceCode(connect.getCode())
|
||||||
|
// .alias(config3.getAlias())
|
||||||
|
// .aliasName(config3.getAliasName())
|
||||||
|
// .registerAddress(config3.getRegisterAddress())
|
||||||
|
// .dataType(config3.getDataType())
|
||||||
|
// .readonly(config3.getReadonly())
|
||||||
|
// .build();
|
||||||
|
//
|
||||||
|
// RValue result3 = plcS7ProtocolDriver.read(deviceBO, siteBO3);
|
||||||
|
// System.out.println("地址: " + config3.getRegisterAddress());
|
||||||
|
// System.out.println("数据类型: " + config3.getDataType());
|
||||||
|
// System.out.println("读取结果: " + result3.getValue());
|
||||||
|
|
||||||
|
System.out.println("\n========== PLC S7读取测试完成 ==========");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("PLC S7读取测试失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user