场景
SpringBoot+modbus4j实现ModebusTCP通讯读取数据:
https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/135292378
基于上面对单个PLC设备进行ModbusTCP通讯获取数据的过程。
业务开发中,如果需要定时读取多个设备的数据,且需要将设备数据存储进redis中缓存,以供其他业务功能使用。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
实现
新建SpringBoot项目并添加需要的pom依赖
<!--modbus4j 依赖-->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
其他业务依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
<!-- 排除SpringBoot的版本管理 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--modbus4j 依赖-->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.6</version>
</dependency>
配置文件中新增多个PLC设备相关信息,这里是三个,只关注必要连接相关信息,其他冗余信息忽略:
#plc相关配置
modbus:
task:
rate: "10000" # 定时任务执行周期
timeout: 3000 # 读取超时(ms)
# PLC寄存器配置
devices:
- ip: 127.0.0.1
port: 502
slaveId: 1
name: "#1"
registers:
runSignal:
address: "400153.0" # V152.0运行信号
type: "bool"
pressure:
address: "400201" # VD200运行压力
type: "float32"
cameras:
recognition:
ip: "127.0.0.1"
port: 8000
username: "admin"
password: "123456"
channel: 1
userId: 1
capture:
ip: "127.0.0.1"
port: 8000
username: "admin"
password: "123456"
channel: 2
userId: 2
- ip: 127.0.0.2
port: 502
slaveId: 2
name: "#2"
registers:
runSignal:
address: "400154.0" # V153.0运行信号
type: "bool"
pressure:
address: "400202" # VD201运行压力
type: "float32"
cameras:
recognition:
ip: "127.0.001"
port: 8000
username: "admin"
password: "123456"
channel: 1
userId: 3
capture:
ip: "127.0.0.1"
port: 8000
username: "admin"
password: "123456"
channel: 2
userId: 4
- ip: 127.0.0.3
port: 502
slaveId: 3
name: "#3"
registers:
runSignal:
address: "400154.0" # V153.0运行信号
type: "bool"
pressure:
address: "400202" # VD201运行压力
type: "float32"
cameras:
recognition:
ip: "127.0.0.1"
port: 8000
username: "admin"
password: "123456"
channel: 1
userId: 5
capture:
ip: "127.0.0.1"
port: 8000
username: "admin"
password: "123456"
channel: 2
userId: 6
增加对应的配制类:
package com.badao.demo.config;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@ConfigurationProperties(prefix = "modbus")
@Configuration
@Getter
@Setter
@Component
public class ModbusConfig {
private transient Map<String, DeviceConfig> recognitionIpToDeviceMap;
@PostConstruct
public void initMapping() {
// 初始化IP映射关系
recognitionIpToDeviceMap = devices.stream()
.filter(device -> device.getCameras() != null)
.filter(device -> device.getCameras().getRecognition() != null)
.collect(Collectors.toMap(
device -> device.getCameras().getRecognition().getIp(),
Function.identity()
));
System.out.println("已初始化摄像头映射关系"+recognitionIpToDeviceMap);
}
public DeviceConfig findDeviceByRecognitionIp(String recognitionIp) {
return recognitionIpToDeviceMap.get(recognitionIp);
}
private TaskConfig task;
private List<DeviceConfig> devices; // 改为List接收
@Data
public static class TaskConfig {
private String rate;
private int timeout;
}
@Data
public static class DeviceConfig {
private String ip; // 设备IP地址
private int port; // 端口号
private int slaveId; // 从站ID
private String name; // 设备名称
private Map<String, RegisterConfig> registers;
// 摄像头绑定配置
private CameraBinding cameras;
@Data
public static class CameraBinding {
private CameraConfig recognition;
private CameraConfig capture;
}
@Data
public static class CameraConfig {
private String ip;
private int port;
private String username;
private String password;
private int channel;
private int userId ;
}
}
@Data
public static class RegisterConfig {
private String address; // 寄存器地址(格式:400153.0 或 400201)
private String type; // 数据类型(bool/float32/int16等)
}
}
新建modbus工具类
package com.badao.demo.utils;
import com.serotonin.modbus4j.BatchRead;
import com.serotonin.modbus4j.BatchResults;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.ip.IpParameters;
import com.serotonin.modbus4j.locator.BaseLocator;
public class Modbus4jUtils {
/**
* 工厂。
*/
static ModbusFactory modbusFactory;
static {
if (modbusFactory == null) {
modbusFactory = new ModbusFactory();
}
}
/**
* 获取master
*
* @return
* @throws ModbusInitException
*/
public static ModbusMaster getMaster(String ip,int port) throws ModbusInitException {
IpParameters params = new IpParameters();
params.setHost(ip);
params.setPort(port);
// modbusFactory.createRtuMaster(wapper); //RTU 协议
// modbusFactory.createUdpMaster(params);//UDP 协议
// modbusFactory.createAsciiMaster(wrapper);//ASCII 协议
//true启用长连接
ModbusMaster master = modbusFactory.createTcpMaster(params, true);// TCP 协议
master.setTimeout(2000); // 超时设置
master.setRetries(1); // 失败后重试1次
master.init();
return master;
}
/**
* 读取[01 Coil Status 0x]类型 开关数据
*
* @param slaveId
* slaveId
* @param offset
* 位置
* @return 读取值
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
*/
public static Boolean readCoilStatus(ModbusMaster master,int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException {
// 01 Coil Status
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, offset);
Boolean value = master.getValue(loc);
return value;
}
/**
* 读取[02 Input Status 1x]类型 开关数据
*
* @param slaveId
* @param offset
* @return
* @throws ModbusTransportException
* @throws ErrorResponseException
*/
public static Boolean readInputStatus(ModbusMaster master,int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException {
// 02 Input Status
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, offset);
Boolean value = master.getValue(loc);
return value;
}
/**
* 读取[03 Holding Register类型 2x]模拟量数据
*
* @param slaveId
* slave Id
* @param offset
* 位置
* @param dataType
* 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
*/
public static Number readHoldingRegister(ModbusMaster master,int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException {
// 03 Holding Register类型数据读取
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
Number value = master.getValue(loc);
return value;
}
/**
* 读取[04 Input Registers 3x]类型 模拟量数据
*
* @param slaveId
* slaveId
* @param offset
* 位置
* @param dataType
* 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return 返回结果
* @throws ModbusTransportException
* 异常
* @throws ErrorResponseException
* 异常
*/
public static Number readInputRegisters(ModbusMaster master,int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException {
// 04 Input Registers类型数据读取
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, offset, dataType);
Number value = master.getValue(loc);
return value;
}
/**
* 批量读取使用方法
*
* @throws ModbusTransportException
* @throws ErrorResponseException
* @throws ModbusInitException
* @return
*/
public static BatchResults<Integer> batchRead(ModbusMaster master,BatchRead<Integer> batchRead) throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 是否连续请求
batchRead.setContiguousRequests(false);
BatchResults<Integer> results = master.send(batchRead);
return results;
}
}
新建定时任务读取数据
package com.badao.demo.task;
import com.badao.demo.config.ModbusConfig;
import com.badao.demo.constants.Constants;
import com.badao.demo.entity.PlcData;
import com.badao.demo.utils.Modbus4jUtils;
import com.badao.demo.utils.RedisCache;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.code.DataType;
import com.serotonin.modbus4j.exception.ErrorResponseException;
import com.serotonin.modbus4j.exception.ModbusInitException;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.PreDestroy;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableScheduling
@Slf4j
@DependsOn("modbusConfig") // 明确依赖配置类
public class GetModbusTCPDataTask {
@Autowired
private ModbusConfig modbusConfig;
@Autowired
private RedisCache redisCache;
// 新增类成员变量(连接池)
private final Map<String, ModbusMaster> masterPool = new ConcurrentHashMap<>();
@Scheduled(fixedRateString = "2000")
public void getData() {
modbusConfig.getDevices().parallelStream().forEach(device -> {
String deviceKey = device.getIp() + ":" + device.getPort();
ModbusMaster master = masterPool.computeIfAbsent(deviceKey, k -> {
try {
return createNewMaster(device);
} catch (ModbusInitException e) {
throw new RuntimeException(e);
}
});
try {
if (!checkMasterValid(master)) {
masterPool.remove(deviceKey);
master = createNewMaster(device); // 重建连接
}
Number runSignal = Modbus4jUtils.readHoldingRegister(master, 1, 0, DataType.TWO_BYTE_INT_UNSIGNED);
Number waterPress = Modbus4jUtils.readHoldingRegister(master, 1, 2, DataType.FOUR_BYTE_FLOAT);
PlcData plcData = PlcData.builder()
.ip(device.getIp())
.runSignal(runSignal.intValue())
.waterPress(waterPress)
.build();
redisCache.setCacheObject(Constants.PLC_KEY+device.getIp(),plcData,5, TimeUnit.SECONDS);
} catch (ModbusTransportException e) {
System.out.println(" 通信失败: 网络或端口不可达 - {}"+e.getMessage());
// 发生通信错误时移除无效连接
masterPool.remove(deviceKey);
master.destroy();
} catch (ErrorResponseException e) {
//System.out.println(" 设备返回错误: 从站ID或寄存器地址无效 - {}"+e.getErrorResponse().getExceptionMessage());
//System.out.println(e.getMessage());
} catch (ModbusInitException e) {
throw new RuntimeException(e);
}
});
}
private ModbusMaster createNewMaster(ModbusConfig.DeviceConfig device) throws ModbusInitException {
try {
return Modbus4jUtils.getMaster(device.getIp(), device.getPort());
} catch (ModbusInitException e) {
throw new RuntimeException("Modbus初始化失败(ip): "+device.getIp() , e);
}
}
//健康检查方法
private boolean checkMasterValid(ModbusMaster master) {
try {
master.init(); // 尝试初始化验证连接状态
return true;
} catch (Exception e) {
return false;
}
}
//在应用关闭时清理连接
@PreDestroy
public void cleanup() {
masterPool.values().forEach(ModbusMaster::destroy);
}
}
上面所用到的实体类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PlcData {
private String ip;
private Number runSignal;
private Number waterPress;
}
所用到的redis工具类
package com.badao.demo.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
*
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @return 缓存的对象
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value);
return operation;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value, timeout, timeUnit);
return operation;
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public void deleteObject(String key)
{
redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection
*/
public void deleteObject(Collection collection)
{
redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
{
ListOperations listOperation = redisTemplate.opsForList();
if (null != dataList)
{
int size = dataList.size();
for (int i = 0; i < size; i++)
{
listOperation.leftPush(key, dataList.get(i));
}
}
return listOperation;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(String key)
{
List<T> dataList = new ArrayList<T>();
ListOperations<String, T> listOperation = redisTemplate.opsForList();
Long size = listOperation.size(key);
for (int i = 0; i < size; i++)
{
dataList.add(listOperation.index(key, i));
}
return dataList;
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
{
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(String key)
{
Set<T> dataSet = new HashSet<T>();
BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
dataSet = operation.members();
return dataSet;
}
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
{
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap)
{
for (Map.Entry<String, T> entry : dataMap.entrySet())
{
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(String key)
{
Map<String, T> map = redisTemplate.opsForHash().entries(key);
return map;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(String pattern)
{
return redisTemplate.keys(pattern);
}
}
yml中redis相关配制
# 数据源
spring:
profiles:
active: default # 强制激活默认配置
application:
name: demo
# redis 配置
redis:
# 地址
#本地测试用
host: 127.0.0.1
port: 6379
password: 123456
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
所用的redis配制类
package com.badao.demo.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置
*
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Primary
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
Redis使用FastJson序列化类
package com.badao.demo.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
@SuppressWarnings("unused")
private ObjectMapper objectMapper = new ObjectMapper();
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJson2JsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
public void setObjectMapper(ObjectMapper objectMapper)
{
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
运行效果:
