Modbus4j库中ModbusFactory类的应用详解

10 次阅读

Modbus4j 是 Java 生态中较常见的 Modbus 协议通信库,适用于通过 Modbus TCP、Modbus RTU 等方式与 PLC、变频器、仪表以及其他工业设备进行数据交互。实际开发中,ModbusFactory 是一个非常重要的入口类,它主要负责创建 Modbus 主站所需的客户端、连接以及请求对象,使开发者不必直接处理大量底层通信细节。

一、ModbusFactory类的作用

ModbusFactory 可以理解为 Modbus4j 中用于创建和管理通信相关对象的工厂类。开发程序通常先创建一个 ModbusFactory 实例,再根据具体通信方式创建 TCP 主站或串口主站。

典型代码如下:

ModbusFactory modbusFactory = new ModbusFactory();

创建工厂对象本身并不会立即建立网络连接或打开串口。真正的通信方式是在后续创建 ModbusMaster 时确定。

例如 Modbus TCP 通常使用:

ModbusMaster master = modbusFactory.createTcpMaster(params, false);

Modbus RTU 则可以通过串口参数创建:

ModbusMaster master = modbusFactory.createRtuMaster(params);

这种设计将“对象创建”和“实际连接”分开,使程序结构更加清晰。

二、创建Modbus TCP主站

Modbus TCP 是工业设备通过以太网进行 Modbus 通信的常见方式。使用 ModbusFactory 创建 TCP 主站时,需要提供 IpParameters。

示例:

IpParameters params = new IpParameters();
params.setHost("192.168.1.100");
params.setPort(502);

ModbusFactory factory = new ModbusFactory();
ModbusMaster master = factory.createTcpMaster(params, false);

其中:

  • host 表示 Modbus TCP 从站设备的 IP 地址;

  • port 通常使用 502;

  • createTcpMaster() 用于创建 TCP 主站;

  • 第二个参数通常用于控制是否保持连接等行为。

创建完成以后,还需要调用 init() 才能开始通信:

master.init();

读取设备数据时,可以进一步构造 Modbus 请求。

三、创建Modbus RTU主站

Modbus RTU 通常通过 RS-485 串口进行通信。相比 TCP,RTU 需要额外配置串口号、波特率、数据位、停止位和校验方式。

例如:

SerialPortWrapper wrapper = new SerialPortWrapper() {
    @Override
    public void open() throws Exception {
    }

    @Override
    public void close() throws Exception {
    }

    @Override
    public int getBaudRate() {
        return 9600;
    }

    @Override
    public int getFlowControlIn() {
        return 0;
    }

    @Override
    public int getFlowControlOut() {
        return 0;
    }

    @Override
    public int getDataBits() {
        return 8;
    }

    @Override
    public int getStopBits() {
        return 1;
    }

    @Override
    public int getParity() {
        return 0;
    }

    @Override
    public InputStream getInputStream() {
        return null;
    }

    @Override
    public OutputStream getOutputStream() {
        return null;
    }
};

实际项目中通常会结合 Modbus4j 提供的串口实现或者项目自己的串口封装,然后交给 ModbusFactory 创建 RTU 主站。

不同版本的 Modbus4j 在串口相关 API 上可能存在差异,因此开发时应结合当前使用版本的接口定义进行实现,不能直接照搬其他版本的示例。

四、ModbusFactory与ModbusMaster的关系

理解两者之间的关系,有助于避免把 ModbusFactory 和真正执行通信的对象混淆。

可以简单理解为:

ModbusFactory
      |
      +---- createTcpMaster()
      |
      +---- createRtuMaster()
      |
      +---- ModbusMaster
                 |
                 +---- init()
                 |
                 +---- send()
                 |
                 +---- destroy()

ModbusFactory 主要负责创建对象,而 ModbusMaster 才是应用程序执行 Modbus 通信的核心对象。

因此,一般业务代码不会通过 ModbusFactory 直接读取寄存器,而是:

ModbusFactory factory = new ModbusFactory();

ModbusMaster master = factory.createTcpMaster(params, false);

master.init();

ModbusRequest request = ...;
ModbusResponse response = master.send(request);

这种职责划分符合工厂模式的基本思想,也降低了业务代码与具体通信实现之间的耦合。

五、ModbusFactory创建主站后的初始化

创建 ModbusMaster 后,很多初学者容易直接调用 send(),却忽略初始化步骤。

比较完整的生命周期通常是:

ModbusFactory factory = new ModbusFactory();

ModbusMaster master = factory.createTcpMaster(params, false);

try {
    master.init();

    // 执行Modbus请求

} finally {
    master.destroy();
}

init() 用于初始化主站所需要的通信资源,而 destroy() 则用于释放连接、线程等相关资源。

如果应用程序长期运行,例如工业网关、采集服务或者设备监控平台,一般不建议每读取一次数据就创建和销毁一个 ModbusMaster。更合理的方式是初始化一次并复用主站对象。

六、使用ModbusFactory读取保持寄存器

创建主站以后,常见操作是读取保持寄存器。以 Modbus 功能码 03 为例,可以使用 ReadHoldingRegistersRequest。

示例:

ReadHoldingRegistersRequest request =
        new ReadHoldingRegistersRequest(1, 0, 10);

ReadHoldingRegistersResponse response =
        (ReadHoldingRegistersResponse) master.send(request);

short[] data = response.getShortData();

这里的参数通常可以理解为:

1   -> 从站地址
0   -> 起始寄存器地址
10  -> 读取寄存器数量

需要注意的是,工业设备文档中的寄存器地址与程序中的地址可能并不是同一种表示方式。例如设备手册可能使用 40001、40002 这样的逻辑地址,而 Modbus 请求往往需要从 0 开始计算的偏移地址。

因此出现“通信成功但读取的数据不正确”时,地址偏移是非常值得优先检查的问题。

七、读取线圈和输入状态

除了保持寄存器,Modbus4j 还支持线圈、离散输入以及输入寄存器等数据类型。

例如读取线圈:

ReadCoilsRequest request =
        new ReadCoilsRequest(1, 0, 8);

ReadCoilsResponse response =
        (ReadCoilsResponse) master.send(request);

boolean[] values = response.getBooleanData();

读取输入寄存器:

ReadInputRegistersRequest request =
        new ReadInputRegistersRequest(1, 0, 5);

ReadInputRegistersResponse response =
        (ReadInputRegistersResponse) master.send(request);

因此,ModbusFactory 更多承担的是通信对象创建工作,而具体读取哪一种数据,则由后续请求对象决定。

八、ModbusFactory与连接参数配置

Modbus TCP 项目中,连接参数配置尤其重要。

常见配置如下:

IpParameters params = new IpParameters();
params.setHost("192.168.10.20");
params.setPort(502);

ModbusFactory factory = new ModbusFactory();
ModbusMaster master = factory.createTcpMaster(params, false);

实际项目中建议不要把 IP 地址和端口直接写死在 Java 代码中,而应该放到配置文件中。

例如:

modbus.host=192.168.10.20
modbus.port=502
modbus.slave-id=1
modbus.timeout=3000

然后由配置类读取这些参数,再创建 ModbusMaster。

这样设备 IP 发生变化时无需重新修改业务代码,也更加适合部署多个采集实例。

九、ModbusFactory创建对象时的超时问题

Modbus 通信涉及网络、串口以及现场设备响应,因此超时配置非常重要。

如果 TCP 设备没有响应,而客户端没有合理的超时机制,业务线程可能长时间等待。

项目中应该根据实际 API 设置通信超时时间,并结合重试次数控制异常情况。

需要注意的是,“连接超时”和“读取数据超时”并不完全是同一个问题。设备能够建立 TCP 连接,并不意味着一定能够正常返回 Modbus 响应。

排查通信问题时可以按照以下顺序检查:

IP是否可达
    ↓
502端口是否开放
    ↓
Modbus TCP是否启用
    ↓
从站地址是否正确
    ↓
寄存器地址是否正确
    ↓
功能码是否被设备支持
    ↓
请求数量是否符合设备限制
    ↓
数据类型和字节序是否正确

这种排查方式通常比单纯修改代码更加有效。

十、ModbusFactory异常处理

工业通信程序不能假设每次请求都会成功。设备断电、网络中断、串口异常、从站忙碌等情况都可能导致请求失败。

因此建议将通信操作放在异常处理结构中:

try {
    master.init();

    ModbusRequest request =
            new ReadHoldingRegistersRequest(1, 0, 2);

    ModbusResponse response = master.send(request);

    // 处理响应

} catch (Exception e) {
    // 记录通信异常
    e.printStackTrace();
} finally {
    master.destroy();
}

生产环境中不建议仅使用 printStackTrace()。更合理的方式是使用日志框架记录设备 IP、从站地址、功能码、寄存器地址和异常原因。

例如日志信息可以包含:

设备:192.168.10.20
从站:1
功能码:03
地址:100
数量:2
错误:读取超时

这些信息能够明显提高现场问题的定位效率。

十一、ModbusFactory在Spring Boot中的使用方式

如果使用 Spring Boot 开发 Modbus 数据采集服务,可以把 ModbusFactory 和 ModbusMaster 交给 Spring 管理。

例如:

@Configuration
public class ModbusConfig {

    @Bean
    public ModbusMaster modbusMaster() {
        IpParameters params = new IpParameters();
        params.setHost("192.168.1.100");
        params.setPort(502);

        ModbusFactory factory = new ModbusFactory();
        ModbusMaster master = factory.createTcpMaster(params, false);
        master.init();

        return master;
    }
}

业务服务中直接注入:

@Service
public class ModbusService {

    private final ModbusMaster master;

    public ModbusService(ModbusMaster master) {
        this.master = master;
    }

    public short[] readData() throws Exception {
        ReadHoldingRegistersRequest request =
                new ReadHoldingRegistersRequest(1, 0, 5);

        ReadHoldingRegistersResponse response =
                (ReadHoldingRegistersResponse) master.send(request);

        return response.getShortData();
    }
}

应用关闭时还需要释放资源。实际项目中可以通过 Spring 生命周期机制统一处理 destroy(),避免连接长期占用。

十二、多设备场景下如何使用ModbusFactory

工业物联网项目中,一个程序往往需要连接多台 Modbus TCP 设备。

这时不建议每次查询都重新执行:

new ModbusFactory()
createTcpMaster()
init()

更适合建立设备连接池或主站管理器,根据设备标识缓存已经初始化的 ModbusMaster。

例如:

Map masters = new ConcurrentHashMap<>();

设备第一次出现时创建主站:

ModbusMaster master = masters.computeIfAbsent(
        deviceIp,
        key -> createMaster(key)
);

后续采集任务直接复用对应的主站对象。

不过,多设备管理时还需要考虑连接断开后的重新初始化、并发访问、设备下线以及资源释放等问题。

十三、并发调用ModbusMaster需要注意什么

很多 Modbus 项目使用定时任务同时采集多个设备,例如:

@Scheduled(fixedDelay = 1000)
public void collect() {
    // 周期性采集设备
}

如果多个线程同时操作同一个主站,需要结合具体 Modbus4j 版本以及底层通信实现确认并发安全边界。

尤其是 Modbus RTU,多个请求共享同一条串口总线,通常不能简单地让多个线程同时发送数据,否则可能产生请求交叉、响应错配等问题。

更稳妥的方案是对同一串口主站的请求进行串行化处理。

TCP 虽然天然适合多连接并发,但同一个设备是否允许并发请求,也要参考设备厂商的 Modbus 实现能力。

十四、ModbusFactory常见问题排查

1. 创建主站成功但无法读取数据

首先检查:

设备IP
TCP端口
从站ID
功能码
寄存器地址
寄存器数量

尤其要确认设备是否真的启用了 Modbus TCP 服务。

2. 抛出连接异常

可以先使用操作系统工具测试目标地址是否可达,再检查 502 端口。

例如:

ping 192.168.1.100

Windows 环境也可以测试端口连通性:

Test-NetConnection 192.168.1.100 -Port 502

如果 TCP 端口都无法建立连接,那么继续调整 Modbus 请求参数通常没有意义。

3. 能读取数据但数值明显错误

这种情况通常与数据解析有关。

例如设备实际保存的是:

IEEE 754 Float

而程序直接将两个 16 位寄存器当成整数处理,最终得到的结果自然不正确。

还需要检查:

  • 高低字节顺序;

  • 高低寄存器顺序;

  • 有符号与无符号类型;

  • 32 位整数;

  • 32 位浮点数;

  • 64 位数据;

  • 缩放系数。

Modbus 协议负责传输寄存器数据,具体业务数据类型通常由设备厂商定义。

十五、ModbusFactory使用中的设计建议

实际开发时,可以把 ModbusFactory 的职责限制在通信层,不要让它直接渗透到业务代码。

比较合理的结构可以是:

Controller
    ↓
Service
    ↓
ModbusClient
    ↓
ModbusMaster
    ↓
Modbus设备

其中 ModbusClient 负责封装:

连接管理
读取寄存器
写入寄存器
读取线圈
写入线圈
异常处理
重连机制
日志记录

业务层只需要关心:

double temperature = modbusClient.readFloat(1, 100);

而不需要知道底层具体使用什么 ModbusRequest。

这种分层方式能够降低业务代码对 Modbus4j API 的依赖,也方便未来替换通信库。

十六、ModbusFactory的核心使用流程

将整个使用过程归纳起来,可以得到一个比较清晰的流程:

创建 ModbusFactory
        ↓
配置 TCP 或 RTU 参数
        ↓
创建 ModbusMaster
        ↓
调用 init()
        ↓
构造 ModbusRequest
        ↓
调用 master.send()
        ↓
解析 ModbusResponse
        ↓
处理业务数据
        ↓
异常重试或重连
        ↓