1 ,项目基于springboot,maven引入:
<dependency>
<groupId>com.github.s7connector</groupId>
<artifactId>s7connector</artifactId>
<version>2.1</version>
</dependency>
2,自定义PLC配置,配置包括多个PLC,每个PLC多DB块(后面设计每个DB块对应一个实体类,用于数据读写),每个DB块包含多种类型数据(每个数据类型对应实体类的成员变量)
package service.connect.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 通过TCP对接plc设备配置
*/
@Configuration
@ConfigurationProperties(prefix = "spring.plc")
public class PLCSetting {
/**
* PLC设备列表
*/
private List<Device> devices;
public List<Device> getDevices() {
return devices;
}
public void setDevices(List<Device> devices) {
this.devices = devices;
}
public static class Device {
/**
* PLC设备IP地址
*/
private String ip;
/**
* PLC设备端口
*/
private Integer port;
/**
* 连接超时,单位:毫秒<br/>
* 默认5000ms
*/
private Integer connectTimeout = 5000;
/**
* 默认0
*/
private Integer rack = 0;
/**
* 默认1
*/
private Integer slot = 1;
/**
* 一个设备可以包含多个数据块
*/
List<DataBlock> dataBlock;
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public Integer getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Integer getRack() {
return rack;
}
public void setRack(Integer rack) {
this.rack = rack;
}
public Integer getSlot() {
return slot;
}
public void setSlot(Integer slot) {
this.slot = slot;
}
public List<DataBlock> getDataBlock() {
return dataBlock;
}
public void setDataBlock(List<DataBlock> dataBlock) {
this.dataBlock = dataBlock;
}
}
public static class DataBlock {
/**
* PLC数据块编号
*/
private Integer databaseNumber;
/**
* 偏移量 默认为0
*/
private Integer offset = 0;
/**
* 是否轮询、重复读取数据,默认:true
*/
private boolean readRepeat = true;
/**
* 数据读取间隔,单位毫秒,默认100ms
*/
private Integer readRepeatInterval = 100;
/**
* 数据对应的实体类对象全路径,数据实体配置数据地址偏移量、长度等相关信息
*/
private String entityClassName;
public Integer getDatabaseNumber() {
return databaseNumber;
}
public void setDatabaseNumber(Integer databaseNumber) {
this.databaseNumber = databaseNumber;
}
public Integer getOffset() {
return offset;
}
public void setOffset(Integer offset) {
this.offset = offset;
}
public boolean isReadRepeat() {
return readRepeat;
}
public void setReadRepeat(boolean readRepeat) {
this.readRepeat = readRepeat;
}
public Integer getReadRepeatInterval() {
return readRepeatInterval;
}
public void setReadRepeatInterval(Integer readRepeatInterval) {
this.readRepeatInterval = readRepeatInterval;
}
public String getEntityClassName() {
return entityClassName;
}
public void setEntityClassName(String entityClassName) {
this.entityClassName = entityClassName;
}
}
}
3,yml配置示例(配置项参考上面的配置类):这里使用两种实体类(ExampleBlock1、ExampleBlock2)写法读取同一个DB块(database-number: 2)的数据
spring:
plc:
devices:
- ip: 192.168.0.1
port: 102
slot: 1
rack: 0
dataBlock:
- database-number: 2
read-repeat: true
read-repeat-interval: 1000
offset: 0
entity-class-name: service.connect.entity.plc.ExampleBlock1
- database-number: 2
read-repeat: true
read-repeat-interval: 1000
offset: 0
entity-class-name: service.connect.entity.plc.ExampleBlock2
4,连接与操作,类比较长,此处做拆分备注。
4.1,项目启动时初始化连接:注入配置,定义配置缓存Map(用于后续执行读写操作),定义线程池(按需而定,我这里需要定时读取数据)
@Resource
private PLCSetting plcSetting;
Map<String, DataBlockInfo> dataBlockInfoMap = new HashMap<>();
private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(16);
@PostConstruct
public void init() {
if (!CollectionUtils.isEmpty(plcSetting.getDevices())) {
plcSetting.getDevices().forEach(plc -> {
S7Connector s7Connector = S7ConnectorFactory
.buildTCPConnector()
.withHost(plc.getIp())
.withPort(plc.getPort())
.withTimeout(plc.getConnectTimeout()) //连接超时时间
.withRack(plc.getRack())
.withSlot(plc.getSlot())
.build();
S7Serializer s7Serializer = S7SerializerFactory.buildSerializer(s7Connector);
if (!CollectionUtils.isEmpty(plc.getDataBlock())) {
plc.getDataBlock().forEach(dataBlock -> {
//开启轮询读取数据
if (dataBlock.isReadRepeat()) {
try {
Class<?> clazz = getClass().getClassLoader().loadClass(dataBlock.getEntityClassName());
executorService.scheduleWithFixedDelay(() -> {
try {
//读取数据
Object data = s7Serializer.dispense(clazz, dataBlock.getDatabaseNumber(), dataBlock.getOffset());
//TODO data
if (logger.isWarnEnabled()) {
logger.info("{}: [{}]", data.getClass(), data);
}
} catch (Exception e) {
e.printStackTrace();
}
}, 1000, dataBlock.getReadRepeatInterval(), TimeUnit.MILLISECONDS);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//缓存连接信息
DataBlockInfo dataBlockInfo = new DataBlockInfo();
dataBlockInfo.setS7Connector(s7Connector);
dataBlockInfo.setS7Serializer(s7Serializer);
dataBlockInfo.setDataBlock(dataBlock);
dataBlockInfoMap.put(dataBlock.getEntityClassName(), dataBlockInfo);
});
}
});
}
}
数据块缓存用的内部类:
public static class DataBlockInfo {
private PLCSetting.DataBlock dataBlock;
private S7Serializer s7Serializer;
private S7Connector s7Connector;
public PLCSetting.DataBlock getDataBlock() {
return dataBlock;
}
public void setDataBlock(PLCSetting.DataBlock dataBlock) {
this.dataBlock = dataBlock;
}
public S7Serializer getS7Serializer() {
return s7Serializer;
}
public void setS7Serializer(S7Serializer s7Serializer) {
this.s7Serializer = s7Serializer;
}
public S7Connector getS7Connector() {
return s7Connector;
}
public void setS7Connector(S7Connector s7Connector) {
this.s7Connector = s7Connector;
}
}
4.2,数据写入
4.2.1,s7connector自带的store方法:通过DB块配置的number、offset以及实体类来保存数据,但是这个方法会刷新实体类所有成员属性对应的数据,如果属性没有赋值,也会覆盖原有的数据。
/**
* 保存PLC数据,整体保存,保存所有数据
*/
public <T> void save(T t) {
if (dataBlockInfoMap.containsKey(t.getClass().getName())) {
DataBlockInfo dataBlockInfo = dataBlockInfoMap.get(t.getClass().getName());
dataBlockInfo.getS7Serializer().store(t, dataBlockInfo.getDataBlock().getDatabaseNumber(), dataBlockInfo.getDataBlock().getOffset());
} else {
logger.error("[{}] 对应的数据块未配置,或者读取PLC配置失败!", t.getClass().getName());
}
}
4.2.2,我自己的需求是当属性为null时不做处理:注意:parseBytes方法中关于值为String类型的处理参考
com.github.s7connector.impl.serializer.converter.StringConverter的insert方法,如有其它需求,可以参考该包下的其它Converter
/**
* 保存PLC数据,单属性保存,保存所有不为null的属性
*/
public <T> void saveBySingle(T t) {
if (dataBlockInfoMap.containsKey(t.getClass().getName())) {
try {
DataBlockInfo dataBlockInfo = dataBlockInfoMap.get(t.getClass().getName());
Arrays.stream(t.getClass().getFields()).forEach(field -> {
try {
if (!ObjectUtils.isEmpty(field.get(t))) {
S7Variable s7Variable = field.getAnnotation(S7Variable.class);
if (!ObjectUtils.isEmpty(s7Variable)) {
byte[] bytes = parseBytes(field.get(t), s7Variable);
dataBlockInfo.getS7Connector().write(DaveArea.DB, dataBlockInfo.getDataBlock().getDatabaseNumber(), s7Variable.byteOffset(), bytes);
} else {
logger.error("类[{}]的属性[{}]需要添加注解标签", t.getClass().getName(), field.getName());
}
}
} catch (IllegalAccessException e) {
logger.error("保存PLC数据失败", e);
}
});
} catch (Exception e) {
e.printStackTrace();
}
} else {
logger.error("[{}] 对应的数据块未配置,或者读取PLC配置失败!", t.getClass().getName());
}
}
private byte[] parseBytes(Object value, S7Variable s7Variable) {
byte[] bytes = null;
if (value instanceof Integer) {
bytes = new byte[2];
intToBytes(((Integer) value).intValue(), bytes);
} else if (value instanceof byte[]) {
bytes = (byte[]) value;
} else if (value instanceof String) {
byte[] buffer = ((String) value).getBytes();
bytes = new byte[buffer.length + 2];
bytes[0] = (byte) s7Variable.size();
bytes[1] = (byte) ((String) value).length();
for (int i = 0; i < buffer.length; ++i) {
bytes[i + 2] = (byte) (buffer[i] & 255);
}
}
return bytes;
}
/**
* PLC中word类型占用2个byte
*/
private void intToBytes(int number, byte[] outBytes) {
outBytes[0] = (byte) (number >> 8);
outBytes[1] = (byte) number;
}
4.2.3,DB块对应的实体类:偏移量不能写错
public class ExampleBlock1 implements Serializable {
@S7Variable(byteOffset = 0, arraySize = 16, type = S7Type.WORD)
public Integer[] integer;
@S7Variable(byteOffset = 32, size = 50, type = S7Type.STRING)
public String string0;
//toString...
}
public class ExampleBlock2 implements Serializable {
@S7Variable(byteOffset = 0, size = 2, type = S7Type.WORD)
public Integer integer0;
@S7Variable(byteOffset = 2, size = 2, type = S7Type.WORD)
public Integer integer1;
// 中间还有很多。。。太长就省略了
@S7Variable(byteOffset = 28, size = 2, type = S7Type.WORD)
public Integer integer14;
@S7Variable(byteOffset = 30, size = 2, type = S7Type.WORD)
public Integer integer15;
@S7Variable(byteOffset = 32, size = 50, type = S7Type.STRING)
public String string0;
//toString...
}
最后来博图的截图
数据读取执行结果:
2022-01-19 16:47:35.256 [pool-6-thread-2] INFO c.d.p.s.connect.service.PLCConnect - class city.dekun.park.service.connect.entity.plc.ExampleBlock2: [integer0[0], integer1[18], integer2[255], integer3[65535], integer4[0], integer5[0], integer6[0], integer7[0], integer8[0], integer9[0], integer10[0], integer11[0], integer12[0], integer13[0], integer14[0], integer15[18], string0[asdfasfd] ]
2022-01-19 16:47:35.287 [pool-6-thread-10] INFO c.d.p.s.connect.service.PLCConnect - class city.dekun.park.service.connect.entity.plc.ExampleBlock1: [integer[[Ljava.lang.Integer;@11f3bbfe], string0[asdfasfd] ]