Java實現RS485串口通信

前言

  前段時間趕項目的過程中,遇到一個調用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只是拋磚引玉,有相關開發需求的童鞋可以看一看,參考一下大概的思路。