最近在工作中遇到物联网接入设备,接触到了Modbus协议,想跟大家分享下学习心得。
网上很多关于Modbus的介绍,我在这里就不多说了,本文采用 modbus4j 实现java 集成 Modbus TCP,Modbus报文是字节或者位传输,但是对于我们java层面来说,我们有封装的工具类,不用直接操作字节或者位。但是一些基本的东西还是要了解,我会以我对Modbus TCP的了解,以大白话的方式跟大家讲明白。
1、Modbus的基本了解
1.1、通讯方式
三种通讯方式:
通讯方式 | 方式介绍 |
---|---|
Modbus-RTU | 使用二进制编码的数据,通过串行端口(如RS-485)进行通信,效率较高 |
Modbus-ASCII | 使用ASCII字符表示数据,通过串行端口进行通信,可读性好但效率较低 |
Modbus-TCP/IP | 基于TCP/IP协议,通过以太网进行通信,具有高速和稳定的特点 |
1.2、信号
首先我们要了解两个信号,也可以理解成两种数据:
信号 | 信号介绍 |
---|---|
开关量 | 就是状态,比如灯的状态,是开,还是关,一般是0和1 |
模拟量 | 就是数据值,比如这个灯的电流、电压、功率,这些就是模拟量 |
1.3、Modbus操作对象
Modbus操作的对象有4个,也可以理解成操作入口:
操作对象 | 对象介绍 |
---|---|
线圈 | 可以读取或者写入开关量。以位为单位 |
离散量 | 只能读取开关量,不能写入开关量。以位为单位 |
输入寄存器 | 只能读取模拟量,不能写入模拟量。是16位的,也就是2字节 |
保持寄存器 | 可以读取或者写入模拟量。是16位的,也就是2字节 |
根据不同的对象,会有不同的功能码,但是还是那句话,java有封装的工具类,快速开发的情况下,可以不用深入去了解这些功能码,我这里就不过多赘述了,网上有很多对于modbus协议的介绍,有兴趣的朋友可以去网上搜索。
1.4、数据传输流程
在Modbus通信的设备分为主站(mater)和从站(slave),主站是下发指令的,从站就是接受指令然后根据指令干活的,对于我们接入数据的程序来说,代码程序就是主站,通过我们的程序下发读写指令到从站,那从站就是如PLC或者其他控制器。但是有一点,从站不能主动向主站发送消息。
而访问从站并且获取数据,需要知道从站的ip、端口、从站id,还有数据地址(就是常说的点号)这4个东西,一个点号对应一个数据,有些比较特殊的数据,是需要读取两个连续的地址,一般是读取寄存器,前面提到了Modbus的数据是字节或者位,那么读取两个连续的寄存器地址,是需要合并读取的数据的,那么就衍生出字节序的概念,分别是Big-endian、Little-endian、Big-endian byte swap、Little-endian byte swap。
四种字节序的详细区别:
假设我们有一个 32 位数据值 0x12345678(4 个字节:12, 34, 56, 78),存储在两个 16 位寄存器(寄存器 1 和寄存器 2)。以下是不同字节序的存储方式:
- Big-endian(大端序,标准 ABCD):
- 定义:高位字节在前,低位字节在后,寄存器顺序自然排列。
- 存储方式:
- 寄存器 1:12 34(高 16 位)
- 寄存器 2:56 78(低 16 位)
- 内存布局(从低地址到高地址):12 34 56 78
- 特点:
- 最常见的 Modbus 字节序,Modbus4J 默认使用这种顺序。
- 直接将寄存器 1 和寄存器 2 拼接,得到正确值 0x12345678。
- 适用场景:大多数 Modbus 设备使用标准大端序,尤其在 real4(FOUR_BYTE_FLOAT)和 int32(FOUR_BYTE_INT_SIGNED)中。
- Little-endian(小端序,DCBA):
- 定义:低位字节在前,高位字节在后,寄存器顺序也反转。
- 存储方式:
- 寄存器 1:78 56(低 16 位)
- 寄存器 2:34 12(高 16 位)
- 内存布局:78 56 34 12
- 特点:
- 寄存器内容和寄存器顺序都与大端序相反。
- 需要将寄存器 1 和寄存器 2 的字节全部反转,才能得到 0x12345678。
- 适用场景:某些 PLC 或非标准设备可能使用小端序,较少见于 Modbus。
- Big-endian byte swap(大端序字节交换,CDAB):
- 定义:寄存器顺序保持正常(寄存器 1 在前,寄存器 2 在后),但每个寄存器内的字节顺序反转。
- 存储方式:
- 寄存器 1:34 12(高 16 位,字节反转)
- 寄存器 2:78 56(低 16 位,字节反转)
- 内存布局:34 12 78 56
- 特点:
- 寄存器之间的顺序是大端(寄存器 1 高位,寄存器 2 低位),但每个寄存器内部是小端。
- 需要对每个寄存器的字节进行交换,才能得到 0x12345678。
- 适用场景:一些设备(特别是某些 Siemens 或 Schneider 设备)在 Modbus 协议中使用这种字节序,常用于 real4 和 int32。
- Little-endian byte swap(小端序字节交换,BADC):
- 定义:寄存器顺序反转(寄存器 2 在前,寄存器 1 在后),且每个寄存器内的字节顺序也反转。
- 存储方式:
- 寄存器 1:56 78(低 16 位,字节反转)
- 寄存器 2:12 34(高 16 位,字节反转)
- 内存布局:12 34 56 78(注意:内存顺序可能因设备而异)
- 特点:
- 寄存器顺序是小端(低位寄存器在前),且每个寄存器内部字节反转。
- 需要先交换寄存器顺序,再交换每个寄存器的字节,才能得到 0x12345678。
- 适用场景:在 Modbus 中极少见,但在某些特殊设备或协议变种中可能出现。
2、模拟工具
2.1、工具下载
我们下载老版本的,下载链接:https://modbustools.com/download-old.html
常用的主站模拟工具:Modbus Poll
版本:7.2.5
激活序列号:5A5742575C5D391A17627B6C010350
常用的从站模拟工具:Modbus Slave
版本是:8.2.3
目前没找到该版本的激活序列号,可以免费试用30天,但是10分钟后会自动断开连接,需要重启软件
2.2、工具激活
激活Modbus Poll
激活成功
2.3、工具测试
2.3.1、创建从站
2.3.1.1、打开Modbus Slave,创建连接
2.3.1.2、配置连接,配置完后,点击ok
2.3.1.3、最左边的就是地址(也就是点号),最右边的就是值,这样就创建好了
2.3.2、创建主站
2.3.2.1、开Modbus Poll,创建连接
2.3.2.2、配置连接,配置好后,点击ok
连接从站成功
2.3.3、测试数据传递
2.3.3.1、模拟从站值同步(刚刚说了从站是不会主动发送数据的,这里因为是软件的原因,修改了值,主站也会同步修改,生产环境还是需要主站主动去轮询)
2.3.3.1.1、我们先修改从站地址是0的值,主站地址是0的值也修改了
2.3.3.1.2、同步成功
2.3.3.2、模拟主站下发指令
2.3.3.2.1、我们再修改主站地址为0的数据为500,模拟下发指令
2.3.3.2.2、下发成功
到这里,我们前置的准备工作就完成了,开始进入代码环节。
3、代码集成
我用的是 SpringBoot3.4.1 + JDK 17 ,其实这里是不需要Springboot的,我是写了个SpringBoot的Demo,主要依赖的依赖包是modbus4j包
3.1、pom.xml
注意:必须要在标签中,引入那两个仓库
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<groupId>com.jyy</groupId>
<artifactId>jyy-modbus</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.7.18</spring-boot.version>
<fastjson2.version>2.0.43</fastjson2.version>
<hutool.version>5.8.35</hutool.version>
<!-- modBus依赖 -->
<modbus4j.version>3.0.3</modbus4j.version>
<modbus_tcp.version>1.1.0</modbus_tcp.version>
</properties>
<dependencies>
<!-- modBus依赖 -->
<dependency>
<groupId>com.infiniteautomation</groupId>
<artifactId>modbus4j</artifactId>
<version>${modbus4j.version}</version>
</dependency>
<dependency>
<groupId>com.digitalpetri.modbus</groupId>
<artifactId>modbus-master-tcp</artifactId>
<version>${modbus_tcp.version}</version>
</dependency>
<!-- springBoot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- Apache Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- hutool 的依赖配置-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>it.sauronsoftware</groupId>
<artifactId>ftp4j</artifactId>
<version>1.7.2</version>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<!-- 指定文件路径 -->
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<!-- **表示任意级目录,*表示任意文件 -->
<include>**/*</include>
</includes>
</resource>
<!-- 定义包含这些资源文件,能在jar包中获取这些文件 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
<!--是否替换资源中的属性-->
<filtering>false</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<!-- 若想引用modbus4j需要引入下列repository id:ias-snapshots id:ias-releases 两个 ,使用默认仓库下载,不要使用阿里云仓库-->
<repository>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>ias-snapshots</id>
<name>Infinite Automation Snapshot Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-snapshot/</url>
</repository>
<repository>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>ias-releases</id>
<name>Infinite Automation Release Repository</name>
<url>https://maven.mangoautomation.net/repository/ias-release/</url>
</repository>
<repository>
<id>com.e-iceblue</id>
<name>e-iceblue</name>
<url>https://repo.e-iceblue.com/nexus/content/groups/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>dev</id>
<properties>
<!-- 环境标识,需要与配置文件的名称相对应 -->
<profiles.active>dev</profiles.active>
</properties>
<activation>
<!-- 默认环境 -->
<activeByDefault>true</activeByDefault>
</activation>
</profile>
</profiles>
</project>
3.2、核心工具类,注释写的很明白,请大家仔细看看
package com.jyy.modbus.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;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* Modbus通讯工具类(生产级改进版)
* <p>
* 核心特性:
* 1. 线程安全的连接池管理
* 2. 引用计数机制防止提前释放连接
* 3. 细粒度的超时控制(连接获取/操作执行)
* 4. 自动重试和指数退避策略
* 5. 定期清理空闲连接
* <p>
* 设计原则:
* - 每个物理连接对应一个ModbusMaster实例
* - 读写操作自动管理连接生命周期
* - 强制超时限制防止阻塞
* - 异常处理区分网络错误和业务错误
*
* @author jyy
* @data 2025-07-01
*/
@Slf4j
public class ModbusUtils {
/**
* Modbus4j工厂实例(线程安全)
*/
private static final ModbusFactory MODBUS_FACTORY = new ModbusFactory();
/**
* 连接池:host:port -> ModbusMaster实例
*/
private static final Map<String, ModbusMaster> MASTER_MAP = new ConcurrentHashMap<>();
/**
* 引用计数器:host:port -> 当前引用数
*/
private static final Map<String, AtomicInteger> CONNECTION_REF_COUNTS = new ConcurrentHashMap<>();
/**
* 连接锁:host:port -> 专用锁对象(解决惊群效应)
*/
private static final Map<String, ReentrantLock> CONNECTION_LOCKS = new ConcurrentHashMap<>();
/**
* 默认TCP超时3秒
*/
private static final int DEFAULT_TIMEOUT = 3000;
/**
* 默认重试次数
*/
private static final int DEFAULT_RETRIES = 3;
/**
* 默认使用封装模式
*/
private static final boolean DEFAULT_ENCAPSULATED = true;
/**
* 默认操作超时5秒
*/
private static final long DEFAULT_OPERATION_TIMEOUT = 5000;
/**
* 连接等待超时10秒
*/
private static final long CONNECTION_WAIT_TIMEOUT = 10000;
/**
* 连接清理线程(单例)
*/
private static final ScheduledExecutorService CLEANUP_EXECUTOR = Executors.newSingleThreadScheduledExecutor();
static {
/*
* 启动定期清理任务(每5分钟执行一次)
* 清理策略:
* 1. 引用计数为0的空闲连接
* 2. 初始化失败或已关闭的连接
*/
CLEANUP_EXECUTOR.scheduleAtFixedRate(
ModbusUtils::cleanupIdleConnections,
5, 5, TimeUnit.MINUTES);
// 添加JVM关闭钩子确保资源释放
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
destroyAll();
CLEANUP_EXECUTOR.shutdownNow();
}));
}
// ----------------------------------------------- 内部方法 ---------------------------------------------------------
/**
* 获取ModbusMaster连接(线程安全 + 超时控制)
* <p>
* 实现要点:
* 1. 使用双重检查锁确保单例
* 2. 引用计数自动递增
* 3. 带超时的锁获取防止死锁
*
* @param host 设备IP地址
* @param port 设备端口
* @return 可用的ModbusMaster实例
* @throws ModbusTransportException 当连接获取超时或初始化失败时抛出
*/
public static ModbusMaster getMaster(String host, int port) throws ModbusTransportException {
String key = getConnectionKey(host, port);
ReentrantLock lock = CONNECTION_LOCKS.computeIfAbsent(key, k -> new ReentrantLock(true));
try {
// 尝试获取锁(带超时)
if (!lock.tryLock(CONNECTION_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)) {
throw new ModbusTransportException("获取Modbus连接超时: " + key);
}
try {
// 增加引用计数(原子操作)
CONNECTION_REF_COUNTS.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet();
// 双重检查锁创建连接
return MASTER_MAP.computeIfAbsent(key, k -> {
try {
ModbusMaster master = createTcpMaster(host, port,
DEFAULT_TIMEOUT, DEFAULT_RETRIES, DEFAULT_ENCAPSULATED);
master.init();
log.info("ModbusMaster连接已创建: {}", key);
return master;
} catch (ModbusInitException e) {
log.error("ModbusMaster初始化失败: {}", key, e);
throw new RuntimeException("ModbusMaster初始化失败", e);
}
});
} finally {
// 释放锁
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ModbusTransportException("获取Modbus连接被中断: " + e.getMessage());
}
}
/**
* 释放连接引用
* <p>
* 注意:
* - 只有引用计数降为0时才实际销毁连接
* - 线程安全的递减操作
*
* @param host host
* @param port 端口
*/
public static void release(String host, int port) {
String key = getConnectionKey(host, port);
AtomicInteger refCount = CONNECTION_REF_COUNTS.get(key);
if (refCount != null && refCount.decrementAndGet() <= 0) {
Object lock = CONNECTION_LOCKS.get(key);
if (lock != null) {
synchronized (lock) {
// 双重检查防止竞态条件
if (refCount.get() <= 0) {
destroyInternal(host, port);
CONNECTION_REF_COUNTS.remove(key);
CONNECTION_LOCKS.remove(key);
}
}
}
}
}
/**
* 带超时的任务执行
* <p>
* 技术要点:
* 1. 使用独立线程池执行任务
* 2. Future.get()实现超时控制
* 3. 异常转换(ExecutionException -> 业务异常)
*
* @param task 要执行的任务
* @param timeout 超时时间
* @param unit 时间单位
* @return 任务执行结果
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
private static <T> T executeWithTimeout(Callable<T> task, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException, ErrorResponseException, ModbusTransportException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<T> future = executor.submit(task);
try {
return future.get(timeout, unit);
} catch (ExecutionException e) {
// 异常类型转换
Throwable cause = e.getCause();
if (cause instanceof ModbusTransportException) {
throw (ModbusTransportException) cause;
}
if (cause instanceof ErrorResponseException) {
throw (ErrorResponseException) cause;
}
throw new RuntimeException("Modbus操作执行异常", cause);
} finally {
// 中断任务
future.cancel(true);
// 立即释放资源
executor.shutdownNow();
}
}
/**
* 清理空闲连接
* <p>
* 策略:
* 1. 遍历所有连接
* 2. 移除引用计数为0且未初始化的连接
* 3. 线程安全的移除操作
*/
public static void cleanupIdleConnections() {
MASTER_MAP.entrySet().removeIf(entry -> {
String key = entry.getKey();
if (CONNECTION_REF_COUNTS.getOrDefault(key, new AtomicInteger(0)).get() <= 0) {
try {
if (!entry.getValue().isInitialized()) {
entry.getValue().destroy();
log.info("清理空闲Modbus连接: {}", key);
CONNECTION_REF_COUNTS.remove(key);
CONNECTION_LOCKS.remove(key);
return true;
}
} catch (Exception e) {
log.warn("清理Modbus连接失败: {}", key, e);
}
}
return false;
});
}
/**
* 销毁所有连接(系统关闭时调用)
*/
public static void destroyAll() {
MASTER_MAP.forEach((key, master) -> {
try {
master.destroy();
log.info("Modbus连接已关闭: {}", key);
} catch (Exception e) {
log.warn("关闭Modbus连接失败: {}", key, e);
}
});
MASTER_MAP.clear();
CONNECTION_REF_COUNTS.clear();
CONNECTION_LOCKS.clear();
}
/**
* 创建TCP Master连接
*
* @param host host
* @param port 端口
* @param timeout 超时时间
* @param retries 重试次数
* @param encapsulated 是否封装
* @return ModbusMaster
*/
private static ModbusMaster createTcpMaster(String host, int port,
int timeout, int retries, boolean encapsulated) {
IpParameters params = new IpParameters();
params.setHost(host);
params.setPort(port);
ModbusMaster master = MODBUS_FACTORY.createTcpMaster(params, encapsulated);
master.setTimeout(timeout);
master.setRetries(retries);
return master;
}
/**
* 生成连接键(host:port格式)
*
* @param host host
* @param port 端口
* @return 连接键
*/
private static String getConnectionKey(String host, int port) {
return host + ":" + port;
}
/**
* 内部销毁方法(无锁版本)
*
* @param host host
* @param port 端口
*/
private static void destroyInternal(String host, int port) {
String key = getConnectionKey(host, port);
ModbusMaster master = MASTER_MAP.remove(key);
if (master != null) {
try {
master.destroy();
log.info("Modbus连接已关闭: {}", key);
} catch (Exception e) {
log.warn("关闭Modbus连接失败: {}", key, e);
}
}
}
// ----------------------------------------------- 操作方法 ---------------------------------------------------------
/**
* 带超时和重试的读取操作
* <p>
* 特性:
* 1. 自动管理连接生命周期(try-with-resources模式)
* 2. 指数退避重试策略
* 3. 精确的超时控制
*
* @param host 设备IP
* @param port 设备端口
* @param locator 数据定位器
* @param maxRetries 最大重试次数
* @param timeoutMs 超时时间(毫秒)
* @return 读取到的数据
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static <T> T readWithRetry(String host, int port, BaseLocator<T> locator, int maxRetries, long timeoutMs)
throws ModbusTransportException,
ErrorResponseException, InterruptedException, TimeoutException {
ModbusMaster master = getMaster(host, port);
try {
return executeWithTimeout(() -> {
int retries = 0;
while (true) {
try {
return master.getValue(locator);
} catch (ModbusTransportException e) {
if (retries++ >= maxRetries) {
throw e;
}
log.warn("Modbus读取失败,第{}次重试...错误: {}", retries, e.getMessage());
// 指数退避(最大不超过1秒)
TimeUnit.MILLISECONDS.sleep(Math.min(1000, timeoutMs / maxRetries));
}
}
}, timeoutMs, TimeUnit.MILLISECONDS);
} finally {
// 确保释放连接
release(host, port);
}
}
/**
* 带超时和重试的批量读取操作
*
* @param host 设备IP
* @param port 设备端口
* @param batchRead 批量读取对象
* @param <T> 泛型
* @return 批量读取结果
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static <T> BatchResults<T> batchReadWithRetry(String host, int port, BatchRead<T> batchRead)
throws ModbusTransportException, ErrorResponseException, InterruptedException, TimeoutException {
ModbusMaster master = getMaster(host, port);
try {
return executeWithTimeout(() -> {
int retries = 0;
while (true) {
try {
return master.send(batchRead);
} catch (ModbusTransportException e) {
if (retries++ >= DEFAULT_RETRIES) {
throw e;
}
log.warn("Modbus读取失败,第{}次重试...错误: {}", retries, e.getMessage());
// 指数退避(最大不超过1秒)
TimeUnit.MILLISECONDS.sleep(Math.min(1000, DEFAULT_OPERATION_TIMEOUT / DEFAULT_RETRIES));
}
}
}, DEFAULT_OPERATION_TIMEOUT, TimeUnit.MILLISECONDS);
} finally {
// 确保释放连接
release(host, port);
}
}
/**
* 带超时和重试的写入操作
* <p>
* 实现逻辑与readWithRetry类似,区别在于:
* 1. 使用setValue而非getValue
* 2. 返回void类型
*
* @param host host
* @param port 端口
* @param locator 数据定位器
* @param value 写入的值
* @param maxRetries 最大重试次数
* @param timeoutMs 超时时间(毫秒)
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static <T> void writeWithRetry(String host, int port, BaseLocator<T> locator, T value, int maxRetries, long timeoutMs)
throws ModbusTransportException,
ErrorResponseException, InterruptedException, TimeoutException {
ModbusMaster master = getMaster(host, port);
try {
executeWithTimeout(() -> {
int retries = 0;
while (true) {
try {
master.setValue(locator, value);
return null;
} catch (ModbusTransportException e) {
if (retries++ >= maxRetries) {
throw e;
}
log.warn("Modbus写入失败,第{}次重试...错误: {}", retries, e.getMessage());
TimeUnit.MILLISECONDS.sleep(Math.min(1000, timeoutMs / maxRetries));
}
}
}, timeoutMs, TimeUnit.MILLISECONDS);
} finally {
release(host, port);
}
}
/**
* 读取[01 Coil Status 0x]类型 开关数据
* 读取线圈状态(简化版,使用默认配置)
*
* @param slaveId slaveId
* @param offset 位置
* @return 读取值
* @throws ModbusTransportException 异常
* @throws ErrorResponseException 异常
*/
public static Boolean readCoilStatus(String host, int port, int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException,
InterruptedException, TimeoutException {
// 01 Coil Status
BaseLocator<Boolean> loc = BaseLocator.coilStatus(slaveId, offset);
return readWithRetry(host, port, loc, DEFAULT_RETRIES, DEFAULT_OPERATION_TIMEOUT);
}
/**
* 读取[02 Input Status 1x]类型 开关数据
*
* @param slaveId slaveId
* @param offset 偏移量
* @return Boolean
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static Boolean readInputStatus(String host, int port, int slaveId, int offset)
throws ModbusTransportException, ErrorResponseException, InterruptedException, TimeoutException {
// 02 Input Status
BaseLocator<Boolean> loc = BaseLocator.inputStatus(slaveId, offset);
return readWithRetry(host, port, loc, DEFAULT_RETRIES, DEFAULT_OPERATION_TIMEOUT);
}
/**
* 读取[03 Holding Register类型 2x]模拟量数据
*
* @param slaveId slave Id
* @param offset 位置
* @param dataType 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return Number
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static Number readHoldingRegister(String host, int port, int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException, InterruptedException, TimeoutException {
// 03 Holding Register类型数据读取
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
return readWithRetry(host, port, loc, DEFAULT_RETRIES, DEFAULT_OPERATION_TIMEOUT);
}
/**
* 读取[04 Input Registers 3x]类型 模拟量数据
*
* @param slaveId slaveId
* @param offset 位置
* @param dataType 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return 返回结果
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static Number readInputRegisters(String host, int port, int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException, InterruptedException, TimeoutException {
// 04 Input Registers类型数据读取
BaseLocator<Number> loc = BaseLocator.inputRegister(slaveId, offset, dataType);
return readWithRetry(host, port, loc, DEFAULT_RETRIES, DEFAULT_OPERATION_TIMEOUT);
}
/**
* 写入数字类型的模拟量(如:写入Float类型的模拟量、Double类型模拟量、整数类型Short、Integer、Long)
*
* @param host host
* @param port 端口
* @param slaveId 设备id
* @param offset 偏移量
* @param value 写入值,Number的子类,例如写入Float浮点类型,Double双精度类型,以及整型short,int,long
* @param dataType com.serotonin.modbus4j.code.DataType
* @throws ModbusTransportException Modbus传输异常
* @throws ErrorResponseException 错误响应异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static void writeHoldingRegister(String host, int port, int slaveId, int offset, Number value, int dataType)
throws ModbusTransportException, ErrorResponseException, InterruptedException, TimeoutException {
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
writeWithRetry(host, port, loc, value, DEFAULT_RETRIES, DEFAULT_OPERATION_TIMEOUT);
}
}
3.3、测试
3.3.1、测试读取数据
流程:我们先将Modbus Slave,从站id为1。保持寄存器地址为0的数据修改成50,再用代码是否读取到。
测试代码:
/**
* 测试读取保持寄存器
*/
@Test
public void testRead() {
try {
String host = "127.0.0.1";
int port = 502;
// 发送报文
Number result = ModbusUtils.readHoldingRegister(
host, // ip
port, // 端口
1, // 从站id是1
0, // 地址是0
DataType.TWO_BYTE_INT_SIGNED // 数据类型是 两字节的带符号的整数
);
log.info("modbus返回:{}", result.intValue());
} catch (Exception e) {
log.error(e.toString());
}
}
测试成功,能读取到数据
3.3.1、测试批量读取数据
流程:我们先将Modbus Slave,从站id为1。保持寄存器地址为0的数据修改成50,地址为1的数据改成100,地址为2的数据改成150,再用代码是否读取到。
测试代码:
/**
* 测试批量读取保持寄存器
*/
@Test
public void testBatchRead() {
try {
String host = "127.0.0.1";
int port = 502;
// 创建批量对象
BatchRead<String> batchRead = new BatchRead<>();
batchRead.addLocator(
"1", // 数据唯一标识,从返回结果中取出数据会用到
BaseLocator.holdingRegister( // 读取保持寄存器
1, // 从站id是1
0, // 地址是0
DataType.TWO_BYTE_INT_SIGNED // 数据类型是 两字节的带符号的整数
)
);
batchRead.addLocator(
"2", // 数据唯一标识,从返回结果中取出数据会用到
BaseLocator.holdingRegister( // 读取保持寄存器
1, // 从站id是1
1, // 地址是1
DataType.TWO_BYTE_INT_SIGNED // 数据类型是 两字节的带符号的整数
)
);
batchRead.addLocator(
"3", // 数据唯一标识,从返回结果中取出数据会用到
BaseLocator.holdingRegister( // 读取保持寄存器
1, // 从站id是1
2, // 地址是2
DataType.TWO_BYTE_INT_SIGNED // 数据类型是 两字节的带符号的整数
)
);
// 发送报文
BatchResults<String> results = ModbusUtils.batchReadWithRetry(host, port, batchRead);
log.info("1号,地址是0,数据modbus返回:{}", results.getValue("1"));
log.info("2号,地址是1,数据modbus返回:{}", results.getValue("2"));
log.info("3号,地址是2,数据modbus返回:{}", results.getValue("3"));
} catch (Exception e) {
log.error(e.toString());
}
}
测试成功,能读取到数据
3.3.2、测试下发数据
流程:我们通过程序将,从站id为1。保持寄存器地址为0的数据修改成666
测试代码:
/**
* 测试写入保持寄存器
*/
@Test
public void testWrite() {
try {
String host = "127.0.0.1";
int port = 502;
ModbusUtils.writeHoldingRegister(
host, // ip
port, // 端口
1, // 从站id是1
0, // 地址是0
666, // 修改的值
DataType.TWO_BYTE_INT_UNSIGNED
);
} catch (Exception e) {
log.error(e.toString());
}
}
测试成功,能下发数据
3.3.3、连续两个点位
至于连续两个点位就不演示了,因为我也没有具体数据,可将修改方法的最后一个入参,这个DataType改成4字节的数据类型,Modbus4j会自动去读取两个连续的点号,并合并成正确的数据。
下面是DataType的内容,我框起来的部分就是4字节的类型,FOUR_BYTE_FLOAT是正常的浮点数4字节,而下面加了后缀的,就是上面我说到的字节序排序的类型,大家根据实际数据,选择对应的DataType。