Java使用modbus4j集成Modbus TCP协议与设备通讯

最近在工作中遇到物联网接入设备,接触到了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)。以下是不同字节序的存储方式:

  1. 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)中。
  2. Little-endian(小端序,DCBA)
    • 定义:低位字节在前,高位字节在后,寄存器顺序也反转。
    • 存储方式
      • 寄存器 1:78 56(低 16 位)
      • 寄存器 2:34 12(高 16 位)
    • 内存布局:78 56 34 12
    • 特点
      • 寄存器内容和寄存器顺序都与大端序相反。
      • 需要将寄存器 1 和寄存器 2 的字节全部反转,才能得到 0x12345678。
    • 适用场景:某些 PLC 或非标准设备可能使用小端序,较少见于 Modbus。
  3. 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。
  4. 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。
在这里插入图片描述

4、仓库地址

gitee:https://gitee.com/jiyaya/jyy-modbus

### Java 中实现 Modbus RTU 通信 #### 使用 JModbus 库实现 Modbus RTU 客户端 为了简化开发过程,通常会使用现有的库来处理底层细节。JModbus 是一个流行的用于 JavaModbus 实现库。 安装依赖项可以通过 Maven 或 Gradle 来完成: 对于 Maven 用户,在 `pom.xml` 文件中加入如下配置: ```xml <dependency> <groupId>org.jpos</groupId> <artifactId>jmodbus</artifactId> <version>2.0.7</version> </dependency> ``` 创建一个新的类并初始化连接设置: ```java import org.modbus4j.Modbus; import org.modbus4j.locator.BaseLocator; public class ModbusClientExample { public static void main(String[] args) throws Exception { String portName = "/dev/ttyUSB0"; // Linux 下串口设备路径 try (SerialConnection connection = new SerialConnection(portName)) { Modbus.setFactory(new RtuMasterFactory()); Master master = Modbus.getRtuMaster(connection); master.setTimeout(1000); // 设置超时时间 int slaveId = 1; // 设备地址 short startAddress = 0; // 寄存器起始位置 short quantityOfRegisters = 10; // 要读取的数量 ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse)master.send( BaseLocator.holdingRegister(slaveId, startAddress, quantityOfRegisters)); System.out.println("Received data:"); for(int i=0;i<quantityOfRegisters;i++){ System.out.printf("%d ",response.getShort(i)); } } catch(Exception e){ e.printStackTrace(); } } } ``` 这段程序展示了如何建立远程设备之间的 Modbus RTU 连接,并从中获取保持寄存器中的数据[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值