程序员潇然 发表于 2022-8-5 11:00:58

java 解析modbus 协议 Modbus4j应用 使用modbus与串口通信

本文记录使用java开发modbus协议程序,与串口进行连接,使用到的开源代码是modbus4j。

Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气 Schneider Electric)于1979年为使用可编程逻辑控制器(PLC)通信而发表。Modbus已经成为工业领域通信协议的业界标准(De facto),并且现在是工业电子设备之间常用的连接方式。

demo代码路径:

`https://gitee.com/crazybytex/modbustmp`

### 依赖下载

rxtxcomm

modbus4j

#### rxtxcom

`http://fizzed.com/oss/rxtx-for-java`

!(data/attachment/forum/202208/05/103304oty39tuo2uuuq32u.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

下载后,注意查看说明:

!(data/attachment/forum/202208/05/103336l9fowib9z48f9pbi.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

> Windows
>
> ---
>
> Choose your binary build - x64 or x86 (based on which version of
> the JVM you are installing to)
>
> NOTE: You MUST match your architecture.You can't install the i386
> version on a 64-bit version of the JDK and vice-versa.
>
> For a JDK installation:
>
> Copy RXTXcomm.jar ---> <JAVA_HOME>\jre\lib\ext
> Copy rxtxSerial.dll ---> <JAVA_HOME>\jre\bin
> Copy rxtxParallel.dll ---> <JAVA_HOME>\jre\bin
>
> Linux
>
> ---
>
> Choose your binary build - x86_64 or i386 (based on which version of
> the JVM you are installing to)
>
> NOTE: You MUST match your architecture.You can't install the i386
> version on a 64-bit version of the JDK and vice-versa.
>
> For a JDK installation on architecture=i386
>
> Copy RXTXcomm.jar ---> <JAVA_HOME>/jre/lib/ext
> Copy librxtxSerial.so ---> <JAVA_HOME>/jre/lib/i386/
> Copy librxtxParallel.so ---> <JAVA_HOME>/jre/lib/i386/
>
> NOTE: For a JDK installation on architecture=x86_64, just change the
> i386 to x86_64 above.

PS:

**如果按照上面的设置,还会提示找不到jar ,可以简单粗暴的直接放在lib目录下**

#### modbus4j

`https://github.com/MangoAutomation/modbus4j`

!(data/attachment/forum/202208/05/103552b0xxaoqstsg7iz4x.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

```xml
<dependency>
    <groupId>com.infiniteautomation</groupId>
    <artifactId>modbus4j</artifactId>
    <version>3.0.3</version>
</dependency>
```

ps:

也可以自己直接下载源代码,直接进行打包使用,然后jar放在lib下

注意,有些maven镜像仓库可能下载不到这个依赖

### 串口启动程序

modbus4j 没有串口相关的实现,需要自行实现相关功能。

demo实现思路:

参数配置:

`com/crazybytex/modbus/util/SerialPortParameter.java`

核心代码:

```java
/**
   * 串口名称(COM0、COM1、COM2等等)
   */
    private String serialPortName;
    /**
   * 波特率
   * 默认:9600
   */
    private int baudRate;
    /**
   * 数据位 默认8位
   * 可以设置的值:SerialPort.DATABITS_5、SerialPort.DATABITS_6、SerialPort.DATABITS_7、SerialPort.DATABITS_8
   * 默认:SerialPort.DATABITS_8
   */
    private int dataBits;
    /**
   * 停止位
   * 可以设置的值:SerialPort.STOPBITS_1、SerialPort.STOPBITS_2、SerialPort.STOPBITS_1_5
   * 默认:SerialPort.STOPBITS_1
   */
    private int stopBits;
    /**
   * 校验位
   * 可以设置的值:SerialPort.PARITY_NONE、SerialPort.PARITY_ODD、SerialPort.PARITY_EVEN、SerialPort.PARITY_MARK、SerialPort.PARITY_SPACE
   * 默认:SerialPort.PARITY_NONE
   */
    private int parity;
```

串口工具类:

`com/crazybytex/modbus/util/SerialPortUtil.java`

核心代码:

```java
    public static SerialPort openSerialPort(SerialPortParameter parameter)
            throws NoSuchPortException, PortInUseException, UnsupportedCommOperationException {
      return openSerialPort(parameter, 2000);
    }

    public static void closeSerialPort(SerialPort serialPort) {
      if (serialPort != null) {
            serialPort.close();
            System.out.println("Port closed" + serialPort.getName());
      }
    }
```

实现类:

`com/crazybytex/modbus/SerialPortWrapperImpl.java`

核心代码:

```java
    private SerialPort serialPort;

    private SerialPortParameter parameter;

    @Override
    public void close() throws Exception {
      SerialPortUtil.closeSerialPort(serialPort);
    }

    @Override
    public void open() throws Exception {
      serialPort = SerialPortUtil.openSerialPort(parameter);
    }
```

详细代码请参考:

`https://gitee.com/crazybytex/modbustmp`

### modbus4j 项目说明

项目源码中,作者已经有了不少示例测试程序了,实现的时候可以参考

!(data/attachment/forum/202208/05/105005wos33jh13c3syss3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

对于modbus协议开发,无非就是按照地址,功能码对寄存器进行读取,作者已经提供了相关功能码的信息发送。

!(data/attachment/forum/202208/05/105138swdt55tbqrj5j4d5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

!(data/attachment/forum/202208/05/105152kzxrrxbkkeal29zy.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

比如

!(data/attachment/forum/202208/05/105238irmubgr4kvebaogn.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "image.png")

所以使用modbus4j 与设备通信,就转变成了使用相关请求和响应实体,调用相关方法而已。

对于response 提供了获取字节、或者short数组、或者boolean值的快捷方法

```java
/*
* ============================================================================
* GNU General Public License
* ============================================================================
*
* Copyright (C) 2006-2011 Serotonin Software Technologies Inc. http://serotoninsoftware.com
* @author Matthew Lohbihler
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program.If not, see <http://www.gnu.org/licenses/>.
*/
package com.serotonin.modbus4j.msg;

import com.serotonin.modbus4j.base.ModbusUtils;
import com.serotonin.modbus4j.exception.ModbusTransportException;
import com.serotonin.modbus4j.sero.io.StreamUtils;
import com.serotonin.modbus4j.sero.util.queue.ByteQueue;

/**
* <p>Abstract ReadResponse class.</p>
*
* @author Matthew Lohbihler
* @version 5.0.0
*/
abstract public class ReadResponse extends ModbusResponse {
    private byte[] data;

    ReadResponse(int slaveId) throws ModbusTransportException {
      super(slaveId);
    }

    ReadResponse(int slaveId, byte[] data) throws ModbusTransportException {
      super(slaveId);
      this.data = data;
    }

    /** {@inheritDoc} */
    @Override
    protected void readResponse(ByteQueue queue) {
      int numberOfBytes = ModbusUtils.popUnsignedByte(queue);
      if (queue.size() < numberOfBytes)
            throw new ArrayIndexOutOfBoundsException();

      data = new byte;
      queue.pop(data);
    }

    /** {@inheritDoc} */
    @Override
    protected void writeResponse(ByteQueue queue) {
      ModbusUtils.pushByte(queue, data.length);
      queue.push(data);
    }

    /**
   * <p>Getter for the field <code>data</code>.</p>
   *
   * @return an array of {@link byte} objects.
   */
    public byte[] getData() {
      return data;
    }

    /**
   * <p>getShortData.</p>
   *
   * @return an array of {@link short} objects.
   */
    public short[] getShortData() {
      return convertToShorts(data);
    }

    /**
   * <p>getBooleanData.</p>
   *
   * @return an array of {@link boolean} objects.
   */
    public boolean[] getBooleanData() {
      return convertToBooleans(data);
    }

    /**
   * <p>toString.</p>
   *
   * @param numeric a boolean.
   * @return a {@link java.lang.String} object.
   */
    public String toString(boolean numeric) {
      if (data == null)
            return "ReadResponse ";
      return "ReadResponse [len=" + (numeric ? data.length / 2 : data.length * 8) + ", " + StreamUtils.dumpHex(data)
                + "]";
    }
}

```

所以可以直接使用这几个快捷方法,获取相关的字节或者short

#### 为什么是short数组?

因为可以进行连续的读多个地址的数据,那么自然可能会返回多个数据,而不是单个short

本文作者:程序员潇然 疯狂的字节X https://crazybytex.com/

### 编程范式

#### 与modbus通信的逻辑就是:

对指定设备Id的设备,指定某个地址,根据功能码,读取指定个数的数据。

#### 使用modbus4j的编程范式就是:

使用项目提供的相关请求和响应实体(内部封装了功能码),指定设备ID以及地址之后,就可以进行数据读取了。

比如提供的样例如下:

```java
    public static void readDiscreteInputTest(ModbusMaster master, int slaveId, int start, int len) {
      try {
            ReadDiscreteInputsRequest request = new ReadDiscreteInputsRequest(slaveId, start, len);
            ReadDiscreteInputsResponse response = (ReadDiscreteInputsResponse) master.send(request);

            if (response.isException())
                System.out.println("Exception response: message=" + response.getExceptionMessage());
            else
                System.out.println(Arrays.toString(response.getBooleanData()));
      }
      catch (ModbusTransportException e) {
            e.printStackTrace();
      }
    }
```

### 改造应用

我的测试项目场景为,大概率读取多个连续的地址,将这几个地址的数据解析到javaBean

通用方法(功能码为4的)如下,ModBusResponse 是自定义的返回实体类

如果是读多个连续地址,直接通过ModBusResponse的子类实现parse方法,解析到具体的字段

如果是读单个地址,或者不实现ModBusResponse,不重写parse方法,可以直接解析返回的字节数组

```java
public static byte[] readInputRegisters(ModbusMaster master, int slaveId, int start, int len, ModBusResponse modBusResponse) {
      try {
            ReadInputRegistersRequest request = new ReadInputRegistersRequest(slaveId, start, len);
            System.out.println("request data:"+request);

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

            if (response.isException()) {
                modBusResponse.setCode(Constant.RET_CODE_FAILED);
                modBusResponse.setMsg(response.getExceptionMessage());
                return null;
            }
            System.out.println("received bytes:"+ByteStreamUtils.bytesToHex(response.getData()));
            System.out.println("received short array:"+Arrays.toString(response.getShortData()));
            modBusResponse.parse(response.getData());
            System.out.println("final response data:"+modBusResponse);
            System.out.println();
            return response.getData();


      } catch (ModbusTransportException e) {
            e.printStackTrace();
      }
      return null;
    }
```

```java
package com.crazybytex.modbus.beans;

import com.serotonin.modbus4j.ModbusMaster;

/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:22
* @Description 请求实体封装
**/
public class ModBusRequest {
    private ModbusMaster modbusMaster;
    private int slaveId;
    private int startOffset;
    private int numberOfRegisters;

    public ModBusRequest(){

    }

    public ModBusRequest(ModbusMaster modbusMaster,int slaveId,int startOffset,int numberOfRegisters){
      this.modbusMaster = modbusMaster;
      this.slaveId = slaveId;
      this.startOffset = startOffset;
      this.numberOfRegisters = numberOfRegisters;
    }

    /**
   * 不设置numberOfRegisters 默认为 1
   * @param modbusMaster
   * @param slaveId
   * @param startOffset
   */
    public ModBusRequest(ModbusMaster modbusMaster,int slaveId,int startOffset){
      this.modbusMaster = modbusMaster;
      this.slaveId = slaveId;
      this.startOffset = startOffset;
      this.numberOfRegisters = 1;
    }

    public ModbusMaster getModbusMaster() {
      return modbusMaster;
    }

    public void setModbusMaster(ModbusMaster modbusMaster) {
      this.modbusMaster = modbusMaster;
    }

    public int getSlaveId() {
      return slaveId;
    }

    public void setSlaveId(int slaveId) {
      this.slaveId = slaveId;
    }

    public int getStartOffset() {
      return startOffset;
    }

    public void setStartOffset(int startOffset) {
      this.startOffset = startOffset;
    }

    public int getNumberOfRegisters() {
      return numberOfRegisters;
    }

    public void setNumberOfRegisters(int numberOfRegisters) {
      this.numberOfRegisters = numberOfRegisters;
    }

    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder("ModBusRequest{");
      sb.append("modbusMaster=").append(modbusMaster);
      sb.append(", slaveId=").append(slaveId);
      sb.append(", startOffset=").append(startOffset);
      sb.append(", numberOfRegisters=").append(numberOfRegisters);
      sb.append('}');
      return sb.toString();
    }
}

```

```java
package com.crazybytex.modbus.beans;

import static com.crazybytex.modbus.Constant.RET_CODE_SUCCESS;

/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:23
* @Description TODO
**/
public class ModBusResponse {
    private int code;
    private String msg;

    public ModBusResponse() {
      code = RET_CODE_SUCCESS;
      msg="success";
    }

    public ModBusResponse(int code,String msg) {
      this.code = code;
      this.msg = msg;
    }

    /**
   * 解析返回的字节数组 转换为对应的实体类默认什么都不做需要处理子类进行
   * @param bytes 需要处理的字节数组
   */
    public void parse(byte[] bytes){

    }

    public int code() {
      return code;
    }

    public void setCode(int code) {
      this.code = code;
    }

    public String msg() {
      return msg;
    }

    public void setMsg(String msg) {
      this.msg = msg;
    }


    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder("ModBusResponse{");
      sb.append("code=").append(code);
      sb.append(", msg='").append(msg).append('\'');
      sb.append('}');
      return sb.toString();
    }
}

```

一个子类实现

```java
package com.crazybytex.modbus.beans;

import com.crazybytex.modbus.util.ByteStreamUtils;

import java.io.ByteArrayInputStream;

/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/3 17:47
* @Description 电池负载电压和电流
* 功能            地址            功能码                描述            单位               倍数
* ------------------------------------------------------------------------------------------------
*
*负载电压          304A(0d12362)04 (只读)             负载电压            V                100
*负载电流          304B(0d12363)04 (只读)             负载电流            A                100
*
*
*
**/
public class BatteryLoadVoltageAndCurrentResponse extends ModBusResponse{


    /**
   * 蓄电池电压
   */
    private int batteryLoadVoltage;



    /**
   * 蓄电池电流
   */
    private double batteryLoadCurrent;

    @Override
    public void parse(byte[] bytes) {
      ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
      try {
            batteryLoadVoltage = ByteStreamUtils.readShort(byteArrayInputStream)/100;

            batteryLoadCurrent = ByteStreamUtils.readShort(byteArrayInputStream)/100.00;

      }catch(Exception e){


      }

    }

    public int getBatteryLoadVoltage() {
      return batteryLoadVoltage;
    }

    public void setBatteryLoadVoltage(int batteryLoadVoltage) {
      this.batteryLoadVoltage = batteryLoadVoltage;
    }

    public double getBatteryLoadCurrent() {
      return batteryLoadCurrent;
    }

    public void setBatteryLoadCurrent(double batteryLoadCurrent) {
      this.batteryLoadCurrent = batteryLoadCurrent;
    }

    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder("BatteryLoadVoltageAndCurrentResponse{");
      sb.append("batteryLoadVoltage=").append(batteryLoadVoltage);
      sb.append(", batteryLoadCurrent=").append(batteryLoadCurrent);
      sb.append('}');
      return sb.toString();
    }
}

```

封装的方法调用:

```java
package com.crazybytex.modbus.service;

import com.serotonin.modbus4j.ModbusMaster;
import com.crazybytex.modbus.beans.BatteryLoadVoltageAndCurrentResponse;
import com.crazybytex.modbus.util.ModbusUtil;

import static com.crazybytex.modbus.Constant.*;

/**
* @Author 程序员潇然 crazybytex.com
* @Date 2022/8/4 9:42
* @Description TODO
**/
public class BatteryService {

    public BatteryLoadVoltageAndCurrentResponse getLoadVoltageAndCurrent(ModbusMaster client, int slaveId){
      BatteryLoadVoltageAndCurrentResponse batteryLoadVoltageAndCurrentResponse = new BatteryLoadVoltageAndCurrentResponse();
      ModbusUtil.readInputRegisters(client,slaveId,ADDRESS_LOAD_VOLTAGE,2,batteryLoadVoltageAndCurrentResponse);
      return batteryLoadVoltageAndCurrentResponse;
    }

}

```

这样就可以直接通过javabean获取返回结果了

完整代码示例:

`https://gitee.com/crazybytex/modbustmp`

!(data/attachment/forum/202206/16/141330jha7st9soow8772i.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "common_log.png")
`转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-128-1-1.html `
页: [1]
查看完整版本: java 解析modbus 协议 Modbus4j应用 使用modbus与串口通信