天天看點

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

Modus協定是由MODICON(現為施耐德電氣公司的一個品牌)在1979年開發的,是全球第一個真正用于工業現場的總線協定,應用非常廣泛,可謂大名鼎鼎。

理論性的東西就不多介紹了,推薦一本書《Modbus軟體開發實戰指南》,楊更更著,寫得非常好,從理論到實戰,手把手教你玩轉Modbus,不過代碼實戰部分使用的是C#,筆者沒練過這項武功,還是看一下Java中怎麼應用吧,網上資料多用Modbus4J,就選它了。

Modbus4J源代碼:https://github.com/infiniteautomation/modbus4j

Modbus4J沒有提供底層序列槽驅動,是以需要先掌握一些Java序列槽程式設計的能力,快速入門可以參考筆者之前寫的《Java序列槽程式設計例子》這篇文章,本文就是在此基礎上進行的,也應用了其中的序列槽程式設計工具類代碼。

建立項目

建立modbus4j項目,如下圖所示:

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

源代碼位址:https://github.com/wu-boy/modbus4j.git

測試步驟

實作底層序列槽驅動

Modbus4J沒有提供底層序列槽驅動,是以使用序列槽工具類SerialPortUtils來打開和關閉序列槽,代碼如下:

public class SerialPortUtils {

    private static Logger log = LoggerFactory.getLogger(SerialPortUtils.class);

    /**
     * 打卡序列槽
     * @param portName 序列槽名
     * @param baudRate 波特率
     * @param dataBits 資料位
     * @param stopBits 停止位
     * @param parity 校驗位
     * @return 序列槽對象
     */
    public static SerialPort open(String portName, Integer baudRate, Integer dataBits,
                                  Integer stopBits, Integer parity) {
        SerialPort result = null;
        try {
            // 通過端口名識别端口
            CommPortIdentifier identifier = CommPortIdentifier.getPortIdentifier(portName);
            // 打開端口,并給端口名字和一個timeout(打開操作的逾時時間)
            CommPort commPort = identifier.open(portName, 2000);
            // 判斷是不是序列槽
            if (commPort instanceof SerialPort) {
                result = (SerialPort) commPort;
                // 設定一下序列槽的波特率等參數
                result.setSerialPortParams(baudRate, dataBits, stopBits, parity);
                log.info("打開序列槽{}成功", portName);
            }else{
                log.info("{}不是序列槽", portName);
            }
        } catch (Exception e) {
            log.error("打開序列槽{}錯誤", portName, e);
        }
        return result;
    }

    /**
     * 關閉序列槽
     * @param serialPort
     */
    public static void close(SerialPort serialPort) {
        if (serialPort != null) {
            serialPort.close();
            log.warn("序列槽{}關閉", serialPort.getName());
        }
    }

}
           

實作Modbus4J序列槽包裝器接口

Modbus4J提供了序列槽包裝器接口,但是沒有提供實作,是以自己建立一個實作類SerialPortWrapperImpl,作用是為Modbus4J提供序列槽對象SerialPort和操作序列槽的方法,例如打開/關閉序列槽,擷取序列槽輸入/輸出流等,核心代碼如下:

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

模拟從站裝置

RtuSlaveTest類模拟了一個位址為1的從站裝置,使用序列槽“COM2“(請提前使用虛拟序列槽軟體Virtual Serial Port Driver模拟出來COM1和COM2序列槽),通過ModbusFactory建立RtuSlave,然後模拟線圈狀态、離散輸入狀态、保持寄存器和輸入寄存器的資料,代碼中有詳細注釋,代碼如下:

public class RtuSlaveTest {

    public static void main(String[] args) {
        createRtuSlave();
    }

    public static void createRtuSlave(){
        // 設定序列槽參數,序列槽是COM2,波特率是9600
        SerialPortWrapperImpl wrapper = new SerialPortWrapperImpl("COM2", 9600,
                SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE, 0, 0);

        // Modbus工廠,可以建立RTU、TCP等不同類型的Master和Slave
        ModbusFactory modbusFactory = new ModbusFactory();

        final ModbusSlaveSet slave = modbusFactory.createRtuSlave(wrapper);

        // 這玩意網上有人叫做過程影像區,其實就是寄存器
        // 寄存器裡可以設定線圈狀态、離散輸入狀态、保持寄存器和輸入寄存器
        // 這裡設定了從站裝置ID是1
        BasicProcessImage processImage = new BasicProcessImage(1);
        processImage.setInvalidAddressValue(Short.MIN_VALUE);
        slave.addProcessImage(processImage);

        // 添加監聽器,監聽slave線圈狀态和保持寄存器的寫入
        processImage.addListener(new MyProcessImageListener());

        setCoil(processImage);
        setInput(processImage);
        setHoldingRegister(processImage);
        setInputRegister(processImage);

        // 開啟線程啟動從站裝置
        new Thread(() -> {
            try {
                slave.start();
            }
            catch (ModbusInitException e) {
                e.printStackTrace();
            }
        }).start();

        /*new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                // 間隔1秒修改從站裝置1的保持寄存器資料
                updateHoldingRegister(slave.getProcessImage(1));
            }
        }, 1000, 1000);*/
    }

    private static void setCoil(ProcessImage processImage){
        // 模拟線圈狀态
        processImage.setCoil(0, true);
        processImage.setCoil(1, false);
        processImage.setCoil(2, true);
    }

    private static void setInput(ProcessImage processImage){
        // 模拟離散輸入狀态
        processImage.setInput(0, false);
        processImage.setInput(1, true);
        processImage.setInput(2, false);
    }

    private static void setHoldingRegister(ProcessImage processImage){
        // 模拟保持寄存器的值
        processImage.setHoldingRegister(0,(short) 11);
        processImage.setHoldingRegister(1,(short) 22);
        processImage.setHoldingRegister(2,(short) 33);
    }

    private static void updateHoldingRegister(ProcessImage processImage){
        // 模拟修改保持寄存器的值
        processImage.setHoldingRegister(0, (short) RandomUtil.randomInt(0, 100));
        processImage.setHoldingRegister(1,(short) RandomUtil.randomInt(0, 100));
        processImage.setHoldingRegister(2,(short) RandomUtil.randomInt(0, 100));
    }

    private static void setInputRegister(ProcessImage processImage){
        // 模拟輸入寄存器的值
        processImage.setInputRegister(0,(short) 44);
        processImage.setInputRegister(1,(short) 55);
        processImage.setInputRegister(2,(short) 66);
    }
}
           

使用Modbus Poll測試模拟的從站裝置

Modbus Poll和Modbus Slave分别是主站裝置仿真工具和從站裝置仿真工具,是Modbus開發最常用的兩個測試軟體,下載下傳位址:https://www.modbustools.com/。

網上最近出現了一個國産軟體Mthings,能夠同時支援模拟主從機功能,據說功能強大還有使用手冊,免安裝免費使用!筆者由于參考了《Modbus軟體開發實戰指南》這本書,就沒使用Mthings,有興趣的同學可以試用。

  1. 設定連接配接參數

下載下傳安裝後,打開連接配接參數進行設定,如下圖所示:

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

RtuSlaveTest類使用了序列槽COM2來模拟從站裝置,是以這裡選擇COM1,選擇RTU模式,點選OK。

  1. 定義讀寫規則

選擇菜單【Setup】->【Read/Write Definition…】,如下圖所示:

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

設定從站裝置位址為1,功能碼03是讀取保持寄存器資料,寄存器位址為0,數量為3,因為RtuSlaveTest程式中模拟了3個資料,點選OK,如下圖所示:

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料

可以看到讀取到了RtuSlaveTest程式中模拟的3個寄存器的資料,注意别忘了先啟動RtuSlaveTest程式!

選擇不同的功能碼就可以讀取不同的資料,01讀取線圈狀态,02讀取離散輸入狀态,03讀取保持寄存器,04讀取輸入寄存器。

模拟主站裝置

實際開發中可能更多的是開發主站裝置程式,RtuMasterTest代碼如下:

public class RtuMasterTest {

    public static void main(String[] args) throws Exception{
        createRtuMaster();
    }

    private static void createRtuMaster() throws Exception{
        // 設定序列槽參數,序列槽是COM1,波特率是9600
        SerialPortWrapperImpl wrapper = new SerialPortWrapperImpl("COM1", 9600,
                SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE, 0, 0);
        ModbusFactory modbusFactory = new ModbusFactory();
        ModbusMaster master = modbusFactory.createRtuMaster(wrapper);
        master.init();

        // 從站裝置ID是1
        int slaveId = 1;

        // 讀取保持寄存器
        readHoldingRegisters(master, slaveId, 0, 3);
        // 将位址為0的保持寄存器資料修改為0
        writeRegister(master, slaveId, 0, 0);
        // 再讀取保持寄存器
        readHoldingRegisters(master, slaveId, 0, 3);
    }

    private static void readHoldingRegisters(ModbusMaster master, int slaveId, int start, int len) throws Exception{
        ReadHoldingRegistersRequest request = new ReadHoldingRegistersRequest(slaveId, start, len);
        ReadHoldingRegistersResponse response = (ReadHoldingRegistersResponse) master.send(request);
        if (response.isException()){
            System.out.println("讀取保持寄存器錯誤,錯誤資訊是" + response.getExceptionMessage());
        }else {
            System.out.println("讀取保持寄存器=" + Arrays.toString(response.getShortData()));
        }
    }

    private static void writeRegister(ModbusMaster master, int slaveId, int offset, int value) throws Exception{
        WriteRegisterRequest request = new WriteRegisterRequest(slaveId, offset, value);
        WriteRegisterResponse response = (WriteRegisterResponse) master.send(request);
        if (response.isException()){
            System.out.println("寫保持寄存器錯誤,錯誤資訊是" + response.getExceptionMessage());
        }else{
            System.out.println("寫保持寄存器成功");
        }
    }
}
           

先啟動RtuSlaveTest從站裝置模拟程式,再啟動RtuMasterTest主站裝置模拟程式,可以看到雙方控制台均有預期輸出,RtuMasterTest能夠讀寫RtuSlaveTest中的資料。

參考資料

1、初探ModBus4j簡單使用指南

2、使用java的modbus4j的Rtu方式擷取監測資料

3、Modbus java slave app

使用Modbus4J進行RTU模式序列槽通信建立項目測試步驟參考資料