引言
在工业物联网(IIoT)场景中,Modbus 作为最常用的工业通信协议之一,广泛应用于PLC、传感器、仪表等设备的通信。Spring Boot 作为Java生态中最流行的快速开发框架,结合 Modbus 协议可实现设备数据的便捷采集与管理。本文将以 Modbus TCP 协议(最常用场景)为例,详细讲解如何在 Spring Boot 中集成 Modbus 客户端,解析点表并获取设备数据。
一、Modbus 协议基础
1.1 协议核心概念
Modbus 是一种主从(Master-Slave)通信协议,支持两种主要传输方式:
- Modbus RTU:基于串口(RS485/RS232),数据帧为二进制格式;
- Modbus TCP:基于以太网(TCP/IP),数据帧封装在TCP报文中(MBAP头+RTU数据)。
本文聚焦 Modbus TCP ,其通信流程为:
Spring Boot应用(Master) → TCP连接 → 设备(Slave) → 响应数据
1.2 数据地址与功能码
Modbus 设备的数据按"地址"存储,常见功能码对应数据类型:
功能码 | 数据类型 | 地址范围示例 | 说明 |
---|---|---|---|
01 | 读线圈(Coil) | 0-9999 | 开关量输出(布尔值) |
02 | 读离散输入(Input) | 0-9999 | 开关量输入(布尔值) |
03 | 读保持寄存器(Holding) | 40001-49999 | 可读写的多字节数值 |
04 | 读输入寄存器(Input) | 30001-39999 | 只读的多字节数值 |
注意 :Modbus 地址是"虚拟地址",实际设备寄存器偏移量为 地址-1
(例如地址40001对应寄存器偏移0)。
二、环境准备与依赖配置
2.1 开发环境
- JDK 1.8+
- Spring Boot 2.7.x/3.0+(本文以3.0为例)
- Maven 3.8+
2.2 核心依赖
选择 modbus4j 作为Modbus客户端库(功能全面、社区活跃),在 pom.xml
中添加:
xml
<dependencies>
<!-- Spring Boot 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Modbus4j 核心库 -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>3.0.6</version> <!-- 最新稳定版 -->
</dependency>
<!-- 日志依赖(可选) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
三、Modbus 连接与点表数据读取实现
3.1 点表定义与存储
点表是设备数据的元信息,通常包含地址、功能码、数据类型、长度等。实际项目中建议从数据库或配置文件(如YAML)读取,本文以内存对象为例:
arduino
// 点表实体类(示例)
@Data
public class ModbusPoint {
private String pointId; // 点表ID(如"TEMP_001")
private String slaveId; // 设备从机ID(Modbus从机地址,默认1)
private int functionCode; // 功能码(如3=读保持寄存器)
private int address; // Modbus地址(如40001)
private int length; // 读取长度(寄存器数量,如2表示2个寄存器)
private String dataType; // 数据类型(如"FLOAT_32"、"INT_16")
}
3.2 Modbus 连接管理器
封装Modbus连接逻辑,支持连接的创建、关闭和复用(避免频繁创建连接):
java
@Component
public class ModbusConnectionManager {
private static final Logger log = LoggerFactory.getLogger(ModbusConnectionManager.class);
private ModbusMaster master; // Modbus4j的主站对象
/**
* 初始化Modbus TCP连接
* @param host 设备IP(如"192.168.1.100")
* @param port 设备端口(默认502)
*/
public void connect(String host, int port) {
try {
// 创建Modbus工厂
ModbusFactory factory = new ModbusFactory();
// 建立TCP连接
TcpSlave tcpSlave = new TcpSlave(host, port);
master = factory.createRtuMaster(tcpSlave); // 注意:Modbus4j中TCP使用RtuMaster
master.setTimeout(2000); // 设置超时时间(毫秒)
log.info("Modbus TCP连接成功:{}:{}", host, port);
} catch (Exception e) {
log.error("Modbus连接失败", e);
throw new RuntimeException("Modbus连接失败", e);
}
}
/**
* 关闭连接
*/
public void close() {
if (master != null) {
try {
master.destroy();
log.info("Modbus连接已关闭");
} catch (Exception e) {
log.error("关闭连接异常", e);
}
}
}
/**
* 获取Modbus主站对象(单例)
*/
public ModbusMaster getMaster() {
if (master == null) {
throw new IllegalStateException("Modbus未连接,请先调用connect()");
}
return master;
}
}
3.3 点表数据读取服务
核心服务类,负责根据点表信息读取设备数据,并处理数据转换:
ini
@Service
public class ModbusDataService {
private static final Logger log = LoggerFactory.getLogger(ModbusDataService.class);
@Autowired
private ModbusConnectionManager connectionManager;
/**
* 读取单个点的数据
* @param point 点表对象
* @return 数据值(对象类型,如Integer、Float)
*/
public Object readPoint(ModbusPoint point) {
ModbusMaster master = connectionManager.getMaster();
try {
// 根据功能码和地址构建请求
int slaveId = Integer.parseInt(point.getSlaveId());
int offset = point.getAddress() - 1; // 地址转偏移量(如40001→0)
// 读取数据(根据功能码分支处理)
switch (point.getFunctionCode()) {
case 1: // 读线圈(布尔值)
boolean[] coils = master.readCoils(slaveId, offset, point.getLength());
return coils[0]; // 假设只读1个线圈
case 3: // 读保持寄存器(多字节数值)
int[] registers = master.readHoldingRegisters(slaveId, offset, point.getLength());
return convertRegisters(registers, point.getDataType());
default:
throw new UnsupportedOperationException("不支持的功能码:" + point.getFunctionCode());
}
} catch (Exception e) {
log.error("读取点{}失败,地址:{},功能码:{}", point.getPointId(), point.getAddress(), point.getFunctionCode(), e);
throw new RuntimeException("读取点数据失败", e);
}
}
/**
* 批量读取多个点的数据(优化性能)
* @param points 点表列表
* @return 数据映射(点ID→值)
*/
public Map<String, Object> batchReadPoints(List<ModbusPoint> points) {
Map<String, Object> result = new HashMap<>();
// 按从机ID分组,减少连接切换开销
Map<Integer, List<ModbusPoint>> slaveGroup = points.stream()
.collect(Collectors.groupingBy(p -> Integer.parseInt(p.getSlaveId())));
for (Map.Entry<Integer, List<ModbusPoint>> entry : slaveGroup.entrySet()) {
int slaveId = entry.getKey();
List<ModbusPoint> slavePoints = entry.getValue();
try {
// 批量读取寄存器(仅示例,线圈需单独处理)
Set<Integer> allOffsets = slavePoints.stream()
.map(p -> p.getAddress() - 1)
.collect(Collectors.toSet());
int maxOffset = allOffsets.stream().max(Integer::compare).orElse(0);
int totalLength = maxOffset + 1; // 总寄存器数量
int[] allRegisters = master.readHoldingRegisters(slaveId, 0, totalLength);
// 按点表解析数据
for (ModbusPoint point : slavePoints) {
int offset = point.getAddress() - 1;
int[] registers = Arrays.copyOfRange(allRegisters, offset, offset + point.getLength());
result.put(point.getPointId(), convertRegisters(registers, point.getDataType()));
}
} catch (Exception e) {
log.error("批量读取从机{}失败", slaveId, e);
}
}
return result;
}
/**
* 寄存器数据转换为实际值(关键逻辑)
* @param registers 寄存器数组
* @param dataType 数据类型
* @return 转换后的值
*/
private Object convertRegisters(int[] registers, String dataType) {
switch (dataType.toUpperCase()) {
case "INT_16": // 16位有符号整数
return registers[0];
case "UINT_16": // 16位无符号整数
return registers[0] & 0xFFFF;
case "INT_32": // 32位有符号整数(大端)
return ((registers[0] & 0xFFFF) << 16) | (registers[1] & 0xFFFF);
case "UINT_32": // 32位无符号整数(大端)
return ((long)(registers[0] & 0xFFFF) << 16) | (registers[1] & 0xFFFF);
case "FLOAT_32": // 32位浮点数(大端,需处理字节序)
byte[] bytes = new byte[4];
bytes[0] = (byte) ((registers[0] >> 8) & 0xFF);
bytes[1] = (byte) (registers[0] & 0xFF);
bytes[2] = (byte) ((registers[1] >> 8) & 0xFF);
bytes[3] = (byte) (registers[1] & 0xFF);
return ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat();
default:
throw new IllegalArgumentException("不支持的数据类型:" + dataType);
}
}
}
四、Spring Boot 集成与测试
4.1 配置类与启动初始化
在 application.yml
中配置设备参数:
yaml
modbus:
device:
ip: 192.168.1.100 # 设备IP
port: 502 # 设备端口
slave-id: 1 # 从机ID(默认1)
创建配置类加载参数并初始化连接:
kotlin
@Configuration
public class ModbusConfig {
@Value("${modbus.device.ip}")
private String deviceIp;
@Value("${modbus.device.port}")
private int devicePort;
@Autowired
private ModbusConnectionManager connectionManager;
@PostConstruct
public void init() {
connectionManager.connect(deviceIp, devicePort);
}
}
4.2 测试接口
编写Controller提供测试接口,验证数据读取:
less
@RestController
@RequestMapping("/modbus")
public class ModbusController {
@Autowired
private ModbusDataService dataService;
// 测试单个点读取(示例点表ID为"TEMP_001")
@GetMapping("/read/{pointId}")
public Object readPoint(@PathVariable String pointId) {
// 假设从数据库或缓存获取点表(此处硬编码示例)
ModbusPoint point = new ModbusPoint();
point.setPointId(pointId);
point.setSlaveId("1");
point.setFunctionCode(3); // 读保持寄存器
point.setAddress(40001); // 地址40001(偏移0)
point.setLength(2); // 读取2个寄存器(32位浮点数)
point.setDataType("FLOAT_32");
return dataService.readPoint(point);
}
// 测试批量读取(示例点表列表)
@GetMapping("/batch-read")
public Map<String, Object> batchRead() {
List<ModbusPoint> points = new ArrayList<>();
// 添加多个点表...
return dataService.batchReadPoints(points);
}
}
五、常见问题与优化
5.1 常见问题排查
-
连接失败 :检查设备IP、端口是否开放(可通过
telnet <ip> 502
测试);确认设备是否启用Modbus TCP服务。 -
数据读取错误:
- 地址偏移错误(如40001对应偏移0,而非1);
- 功能码与数据类型不匹配(如用03功能码读线圈);
- 字节序问题(设备可能使用小端,需调整
ByteBuffer.order(ByteOrder.LITTLE_ENDIAN)
)。
-
超时或无响应 :增大
master.setTimeout()
的超时时间;检查网络延迟或设备负载。
5.2 性能优化
- 批量读取:优先使用批量读取(如读取多个连续寄存器),减少TCP报文交互次数。
- 连接复用:保持长连接(而非每次读取都新建连接),降低握手开销。
- 异步处理 :对实时性要求不高的场景,使用
@Async
注解异步读取。
六、总结
本文详细讲解了Spring Boot集成Modbus TCP协议的核心步骤,包括依赖配置、连接管理、点表解析及数据转换。实际项目中需根据设备文档调整点表定义和数据转换逻辑,并增加异常处理、日志监控等功能,确保数据采集的稳定性和可靠性。
扩展建议:
- 对于Modbus RTU(串口)场景,只需将
ModbusFactory
的连接方式改为SerialSlave
,并配置串口参数(波特率、数据位等); - 可结合Spring Task实现定时任务,定期采集设备数据并存储到数据库;
- 使用
modbus-simulator
工具模拟设备,方便开发调试(下载地址)。