[代码段] java 解析modbus 协议 Modbus4j应用 使用modbus与串口通信

技术实战 技术实战 4129 人阅读 | 0 人回复

本文记录使用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

image.png

下载后,注意查看说明:

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

image.png

<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

核心代码:

 /**
     * 串口名称(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

核心代码:

    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

核心代码:

    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 项目说明

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

image.png

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

image.png

image.png

比如

image.png

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

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

/*
 * ============================================================================
 * 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[numberOfBytes];
        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 [null]";
        return "ReadResponse [len=" + (numeric ? data.length / 2 : data.length * 8) + ", " + StreamUtils.dumpHex(data)
                + "]";
    }
}

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

为什么是short数组?

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

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

编程范式

与modbus通信的逻辑就是:

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

使用modbus4j的编程范式就是:

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

比如提供的样例如下:

    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方法,可以直接解析返回的字节数组

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;
    }
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();
    }
}
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();
    }
}

一个子类实现

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();
    }
}

封装的方法调用:

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

common_log.png 转载务必注明出处:程序员潇然,疯狂的字节X,https://crazybytex.com/thread-128-1-1.html

关注下面的标签,发现更多相似文章
    黄小斜学Java

    疯狂的字节X

  • 目前专注于分享Java领域干货,公众号同步更新。原创以及收集整理,把最好的留下。
    包括但不限于JVM、计算机科学、算法、数据库、分布式、Spring全家桶、微服务、高并发、Docker容器、ELK、大数据等相关知识,一起进步,一起成长。
热门推荐
[若依]微服务springcloud版新建增添加一个
[md]若依框架是一个比较出名的后台管理系统,有多个不同版本。
[CXX1300] CMake '3.18.1' was not
[md][CXX1300] CMake '3.18.1' was not found in SDK, PATH, or
海康摄像头接入 wvp-GB28181-pro平台测试验
[md]### 简介 开箱即用的28181协议视频平台 `https://github.c