Java實現RS485串列埠通訊

勝金發表於2021-01-13

前言

  前段時間趕專案的過程中,遇到一個呼叫RS485串列埠通訊的需求,趕完專案因為樓主處理私事,沒來得及完成文章的更新,現在終於可以整理一下當時的demo,記錄下來。

  首先說一下大概需求:這個專案是機器視覺方面的,AI演算法通過攝像頭視訊流檢測畫面中的目標事件,比如:火焰、煙霧、人員離崗、吸菸、打手機、車輛超速等,檢測到目標事件後上傳檢測結果到後臺系統,

後臺系統儲存檢測結果並推送結果到前端,這裡用的是SpringBoot整合WebSocket實現前後端互推訊息,感興趣的同學可以看一看,大家多交流。然後就是今天的主題,系統在推送檢測結果到前端的同時,需要觸發

聲光報警器,現有條件就是系統呼叫支援RS485串列埠的繼電器控制電路,進而達到開啟和關閉報警器的目的。

準備工作

  說了這麼多可能沒什麼具體的概念,下面先列出需要的硬體裝置及準備工作:

  硬體:

  USB串列埠轉換器(現在很多主機和筆記本已經沒有485串列埠的介面了,轉換器淘寶可以買到);

  RS485繼電器(12V,繼電器模組有8個通道,模組的暫存器有對應8個通道的命令);

  聲光報警器(12V);

  12V電源轉換器;

  電線若干;

  驅動:

  USB串列埠轉換驅動;

  看了這些硬體,感覺樓主是電工是吧?沒錯,樓主確實是自己摸索著連線的,下面上圖:

   線路如何接不是本文的重點,用12V的硬體就是因為安全,樓主可以大膽嘗試。。。

  接通硬體裝置後,在系統中檢視串列埠名稱,如下圖,可以看到通訊埠名稱是COM1,其實電腦上每個硬體介面都是有固定名稱的,USB插在不同的USB介面上,系統讀取到的通訊埠名稱就是對應介面的名稱,這裡

的埠名稱要記下來,後面編碼要用到。

   然後是搬磚前的最後一步準備工作:安裝驅動。樓主的USB串列埠轉換器是在淘寶上買的,商家提供驅動,在電腦上正常安裝驅動即可。

開發實現

  首先需要引入rxtx的jar包,Java實現串列埠通訊的依賴,如下:

        <dependency>
            <groupId>org.rxtx</groupId>
            <artifactId>rxtx</artifactId>
            <version>2.1.7</version>
        </dependency>    

  引入jar包後,就可以搬磚了,大概思路如下:

  1、獲取到與串列埠通訊的物件;

  2、開啟對應串列埠的埠並建立連線;

  3、獲取對應通道的命令併傳送;

  4、接收返回的資訊;

  5、關閉埠連線。

  程式碼如下:

package com.XXX.utils;

import com.databus.Log;
import gnu.io.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class RS485Demo extends Thread implements SerialPortEventListener {
    //單例模式提供連線串列埠的物件
    private static RS485Demo getInstance(){
        if (cRead == null){
            synchronized (RS485Demo.class) {
                if (cRead == null) {
                    cRead = new RS485Demo();
                    // 啟動執行緒來處理收到的資料
                    cRead.start();
                }
            }
        }
        return cRead;
    }
//    封裝十六進位制的開啟、關閉命令
    private static final List<byte[]> onOrderList = Arrays.asList(
            new byte[]{0x01, 0x05, 0x00, 0x00, (byte) 0xFF, 0x00, (byte) 0x8C, 0x3A},       new byte[]{0x01, 0x05, 0x00, 0x01, (byte) 0xFF, 0x00, (byte) 0xDD, (byte)0xFA},
            new byte[]{0x01, 0x05, 0x00, 0x02, (byte) 0xFF, 0x00, (byte) 0x2D, (byte)0xFA}, new byte[]{0x01, 0x05, 0x00, 0x03, (byte) 0xFF, 0x00, (byte) 0x7C, 0x3A},
            new byte[]{0x01, 0x05, 0x00, 0x04, (byte) 0xFF, 0x00, (byte) 0xCD,(byte) 0xFB}, new byte[]{0x01, 0x05, 0x00, 0x05, (byte) 0xFF, 0x00, (byte) 0x9C, 0x3B},
            new byte[]{0x01, 0x05, 0x00, 0x06, (byte) 0xFF, 0x00, (byte) 0x6C, 0x3B},       new byte[]{0x01, 0x05, 0x00, 0x07, (byte) 0xFF, 0x00,  0x3D, (byte)0xFB});
    private static final List<byte[]> offOrderList = Arrays.asList(
            new byte[]{0x01, 0x05, 0x00, 0x00,  0x00, 0x00, (byte) 0xCD, (byte)0xCA},new byte[]{0x01, 0x05, 0x00, 0x01,  0x00, 0x00, (byte) 0x9C, (byte)0x0A},
            new byte[]{0x01, 0x05, 0x00, 0x02,  0x00, 0x00, (byte) 0x6C, (byte)0x0A},new byte[]{0x01, 0x05, 0x00, 0x03,  0x00, 0x00, (byte) 0x3D, (byte)0xCA},
            new byte[]{0x01, 0x05, 0x00, 0x04,  0x00, 0x00, (byte) 0x8C, (byte)0x0B},new byte[]{0x01, 0x05, 0x00, 0x05,  0x00, 0x00, (byte) 0xDD, (byte)0xCB},
            new byte[]{0x01, 0x05, 0x00, 0x06,  0x00, 0x00, (byte) 0x2D, (byte)0xCB},new byte[]{0x01, 0x05, 0x00, 0x07,  0x00, 0x00, (byte) 0x7C, (byte)0x0B});

    // 監聽器,這裡獨立開闢一個執行緒監聽串列埠資料
// 串列埠通訊管理類
    static CommPortIdentifier portId;
    static RS485Demo cRead = null;
    //USB在主機上的通訊埠名稱,如:COM1、COM2等
    static String COMNUM = "";

    static Enumeration<?> portList;
    InputStream inputStream; // 從串列埠來的輸入流
    static OutputStream outputStream;// 向串列埠輸出的流
    static SerialPort serialPort; // 串列埠的引用
    // 堵塞佇列用來存放讀到的資料
    private BlockingQueue<String> msgQueue = new LinkedBlockingQueue<String>();

    /**
     * SerialPort EventListene 的方法,持續監聽埠上是否有資料流
     */
    public void serialEvent(SerialPortEvent event) {

        switch (event.getEventType()) {
            case SerialPortEvent.BI:
            case SerialPortEvent.OE:
            case SerialPortEvent.FE:
            case SerialPortEvent.PE:
            case SerialPortEvent.CD:
            case SerialPortEvent.CTS:
            case SerialPortEvent.DSR:
            case SerialPortEvent.RI:
            case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
                break;
            case SerialPortEvent.DATA_AVAILABLE:// 當有可用資料時讀取資料
                byte[] readBuffer = null;
                int availableBytes = 0;
                try {
                    availableBytes = inputStream.available();
                    while (availableBytes > 0) {
                        readBuffer = RS485Demo.readFromPort(serialPort);
                        String needData = printHexString(readBuffer);
                        System.out.println(new Date() + "真實收到的資料為:-----" + needData);
                        availableBytes = inputStream.available();
                        msgQueue.add(needData);
                    }
                } catch (IOException e) {
                }
            default:
                break;
        }
    }

    /**
     * 從串列埠讀取資料
     *
     * @param serialPort 當前已建立連線的SerialPort物件
     * @return 讀取到的資料
     */
    public static byte[] readFromPort(SerialPort serialPort) {
        InputStream in = null;
        byte[] bytes = {};
        try {
            in = serialPort.getInputStream();
            // 緩衝區大小為一個位元組
            byte[] readBuffer = new byte[1];
            int bytesNum = in.read(readBuffer);
            while (bytesNum > 0) {
                bytes = concat(bytes, readBuffer);
                bytesNum = in.read(readBuffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                    in = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bytes;
    }

    /**
     * 通過程式開啟COM串列埠,設定監聽器以及相關的引數
     * @return 返回1 表示埠開啟成功,返回 0表示埠開啟失敗
     */
    public int startComPort() {
        // 通過串列埠通訊管理類獲得當前連線上的串列埠列表
        try {
            Log.info("開始獲取串列埠。。。");
            portList = CommPortIdentifier.getPortIdentifiers();
            Log.info("獲取串列埠。。。" + portList);
            Log.info("獲取串列埠結果。。。" + portList.hasMoreElements());

            while (portList.hasMoreElements()) {
                // 獲取相應串列埠物件
                Log.info(portList.nextElement());
                portId = (CommPortIdentifier) portList.nextElement();

                System.out.println("裝置型別:--->" + portId.getPortType());
                System.out.println("裝置名稱:---->" + portId.getName());
                // 判斷埠型別是否為串列埠
                if (portId.getPortType() == CommPortIdentifier.PORT_SERIAL) {
                    // 判斷如果COM4串列埠存在,就開啟該串列埠
//                if (portId.getName().equals(portId.getName())) {
                    if (portId.getName().equals(COMNUM)) {
                        try {
                            // 開啟串列埠名字為COM_4(名字任意),延遲為1000毫秒
                            serialPort = (SerialPort) portId.open(portId.getName(), 1000);

                        } catch (PortInUseException e) {
                            System.out.println("開啟埠失敗!");
                            e.printStackTrace();
                            return 0;
                        }
                        // 設定當前串列埠的輸入輸出流
                        try {
                            inputStream = serialPort.getInputStream();
                            outputStream = serialPort.getOutputStream();
                        } catch (IOException e) {
                            e.printStackTrace();
                            return 0;
                        }
                        // 給當前串列埠新增一個監聽器,serialEvent方法監聽串列埠返回的資料
                        try {
                            serialPort.addEventListener(this);
                        } catch (TooManyListenersException e) {
                            e.printStackTrace();
                            return 0;
                        }
                        // 設定監聽器生效,即:當有資料時通知
                        serialPort.notifyOnDataAvailable(true);

                        // 設定串列埠的一些讀寫引數
                        try {
                            // 位元率、資料位、停止位、奇偶校驗位
                            serialPort.setSerialPortParams(9600,
                                    SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
                                    SerialPort.PARITY_NONE);
                        } catch (UnsupportedCommOperationException e) {
                            e.printStackTrace();
                            return 0;
                        }
                        return 1;
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
            Log.info(e);
            return 0;
        }
        return 0;
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        try {
            System.out.println("--------------任務處理執行緒執行了--------------");
            while (true) {
                // 如果堵塞佇列中存在資料就將其輸出
                try {
                    if (msgQueue.size() > 0) {
                        String vo = msgQueue.peek();
                        String vos[] = vo.split("  ", -1);
                        //根據返回資料可以做相應的業務邏輯操作
//                        getData(vos);
//                        sendOrder();
                        msgQueue.take();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 16轉10計算
    public long getNum(String num1, String num2) {
        long value = Long.parseLong(num1, 16) * 256 + Long.parseLong(num2, 16);
        return value;
    }

    // 位元組陣列轉字串
    private String printHexString(byte[] b) {
        StringBuffer sbf = new StringBuffer();
        for (int i = 0; i < b.length; i++) {
            String hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sbf.append(hex.toUpperCase() + "  ");
        }
        return sbf.toString().trim();
    }

    /**
     * 合併陣列
     *
     * @param firstArray  第一個陣列
     * @param secondArray 第二個陣列
     * @return 合併後的陣列
     */
    public static byte[] concat(byte[] firstArray, byte[] secondArray) {
        if (firstArray == null || secondArray == null) {
            if (firstArray != null)
                return firstArray;
            if (secondArray != null)
                return secondArray;
            return null;
        }
        byte[] bytes = new byte[firstArray.length + secondArray.length];
        System.arraycopy(firstArray, 0, bytes, 0, firstArray.length);
        System.arraycopy(secondArray, 0, bytes, firstArray.length, secondArray.length);
        return bytes;
    }

    //num:偶數啟動報警器,奇數關閉報警器
    //commandInfo:偶數開啟,奇數關閉;channel:繼電器通道;comNum:串列埠裝置通訊名稱
    public static void startRS485(int commandInfo,int channel,String comNum) {
        try {
            if(cRead == null){
                cRead = getInstance();
            }
            if (!COMNUM.equals(comNum) && null != serialPort){
                serialPort.close();
                COMNUM = comNum;
            }
            int i = 1;
            if (serialPort == null){
                COMNUM = comNum;
                //開啟串列埠通道並連線
                i = cRead.startComPort();
            }
            if (i == 1){
                Log.info("串列埠連線成功");
                try {
                    //根據提供的文件給出的傳送命令,傳送16進位制資料給儀器
                    byte[] b;
                    if (commandInfo % 2 == 0) {
                        b = onOrderList.get(channel);
                    }else{
                        b = offOrderList.get(channel);
                    }
                    System.out.println("傳送的資料:" + b);
                    System.out.println("發出位元組數:" + b.length);
                    outputStream.write(b);
                    outputStream.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (outputStream != null) {
                            outputStream.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    //每次呼叫完以後關閉串列埠通道
                    if (null != cRead){
                        if (null != serialPort){
                            serialPort.close();
                            serialPort = null;
                        }
                        cRead.interrupt();
                        cRead = null;
                    }
                }
            }else{
                Log.info("串列埠連線失敗");
                return;
            }
        }catch (Exception e){
            e.printStackTrace();
            Log.info("串列埠連線失敗");

        }
    }

    public static void main(String[] args) {
        //開啟通道1的電路,對應裝置名稱COM3
        startRS485(0,1,"COM3");
    }
}

  程式碼比較繁雜,需要有點耐心才能完全瞭解,大家可以從startRS485()函式作為切入點閱讀程式碼。當然,這個demo只是拋磚引玉,有相關開發需求的童鞋可以看一看,參考一下大概的思路。

相關文章