android下變態(tài)的串口通訊

android下的串口通訊,為毛我總遇到這樣的變態(tài)需求呢。

前言

隨著智能化硬件的發(fā)展android跟智能硬件打交道的越來越常見。而串口通訊是硬件之間最常見的通訊方式,所以android下的串口通訊也可能在某些項目中運用到。


串口開源項目

目前android下用到的串口通訊都是由Google提供的開源項目

google串口開源項目地址

此項目下下來正常情況下是能直接使用的。如果想更深入的了解其更多知識可以查看鏈接中的wikis。

但是:直接能使用時因為他已經(jīng)將so已經(jīng)打包生成好了,如果你將.so放在你的項目中你會發(fā)現(xiàn)是不能使用的,原因是因為so中的方法名是通過開源項目的包名+方法名來的。放在你項目中包名都變了,所以so文件將無法找到對應(yīng)的方法的,這樣我們還是得自己生成so文件。


使用

因為開源項目是eclipse的,而我們現(xiàn)在更多使用的是android studio,如果你還沒有使用android studio那就可以直接使用,但是你確定還不使用android studio么?

在android下可以下載一個“串口調(diào)試助手”App可以對你寫的程序做對比。快速驗證問題。

原理

android下的串口通訊是使用jni來使用的,如果你還不太了解Jni使用可以查看我另外一篇文章,其中詳細介紹了JNI的使用。

像小白一樣學(xué)習(xí)JNI

上面這篇文章也是基于串口下的jni寫的,所以本篇文章并不介紹JNI的使用。只是說直接使用串口通訊

原理:Google開源項目直接提供了SerialPort、SerialPortFinder兩個主要的類。這兩個類提供了打開/關(guān)閉串口的方法。然后我們需要將java下的打開和關(guān)閉串口方法生成.h文件,然后實現(xiàn).c方法;然后相應(yīng)的生成.so文件。.so文件沒問題之后才能正常使用。

實現(xiàn)

步驟一:設(shè)置串口號與波特率

我們知道串口通訊都是通過串口號與波特率來的,這跟我們tcp必須知道ip、port一樣的原理。

    public SerialPort getSerialPort() throws SecurityException, IOException, InvalidParameterException {
    if (mSerialPort == null) {
        /* Read serial port parameters */
        SharedPreferences sp = getSharedPreferences("android_serialport_api.sample_preferences", MODE_PRIVATE);
        String path = sp.getString("DEVICE", "");
        int baudrate = Integer.decode(sp.getString("BAUDRATE", "-1"));

        Log.e("TAG","path="+path+"     baudrate="+baudrate);
        /* Check parameters */
        if ( (path.length() == 0) || (baudrate == -1)) {
            throw new InvalidParameterException();
        }

        /* Open the serial port */
        mSerialPort = new SerialPort(new File(path), baudrate, 0);
    }
    return mSerialPort;
}

<code>mSerialPort = new SerialPort(new File(path), baudrate, 0);</code>
這里面有3個參數(shù);第一個參數(shù)為串口路徑,第二個參數(shù)為波特率,第三個為狀態(tài)
前面兩個參數(shù)我們都是知道的一般為:
<code>private static int baudrate = 115200; //波特率

private static String path = "/dev/ttyS3"; //路徑</code>

請根據(jù)自己的為準。

步驟二:打開串口

打開和關(guān)閉串口是使用jni來調(diào)用c方法的。我們在上層調(diào)用打開串口的方法

    Log.e(TAG,"串口打開");
        mSerialPort = serialPortUtil.getSerialPort(TokenCommon.ROBOT_SERIALPORT_BAUDRATE,TokenCommon.ROBOT_SERIALPORT_PATH);
        mOutputStream = mSerialPort.getOutputStream();
        mInputStream = mSerialPort.getInputStream();

        /* Create a receiving thread */
        mReadThread = new ReadThread();
        mReadThread.start();

打開串口成功之后會開啟一個線程一直讀取數(shù)據(jù)。

    private class ReadThread extends Thread {

    @Override
    public void run() {
        super.run();
        while(!isInterrupted()) {
            int size;
            try {
                byte[] buffer = new byte[64];
                if (mInputStream == null) return;
                size = mInputStream.read(buffer);
                if (size > 0) {
                    onDataReceived(buffer, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
        }
    }
}

如果讀取數(shù)據(jù)會調(diào)用onDataReceived方法。onDataReceived個方法為接收方法,如果你需要那個類接收串口數(shù)據(jù)只要實現(xiàn)該方法就好了,下面我們會在數(shù)據(jù)接收的方法中說到

步驟三:發(fā)送串口數(shù)據(jù)

先看開源項目的發(fā)送源碼:

    EditText Emission = (EditText) findViewById(R.id.EditTextEmission);
    Emission.setOnEditorActionListener(new OnEditorActionListener() {
        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            int i;
            CharSequence t = v.getText();
            char[] text = new char[t.length()];
            for (i=0; i<t.length(); i++) {
                text[i] = t.charAt(i);
            }
            try {
                mOutputStream.write(new String(text).getBytes());
                mOutputStream.write('\n');
            } catch (IOException e) {
                e.printStackTrace();
            }
            return false;
        }
    });

因為項目是直接將edittext輸入的字符串轉(zhuǎn)為byte數(shù)據(jù)直接發(fā)送。而我們項目可能是直接發(fā)送byte數(shù)組,所以我修改發(fā)送源碼為:

    private void sendSerialData(){
    String content = ""; //無數(shù)據(jù)內(nèi)容
    int dateLength = ByteUtil.getContentLength(content);//數(shù)據(jù)長度
    byte by[] = new byte[6];
    by[0] = TokenCommon.ANDROIDSENDROBOT;  //協(xié)議頭
    by[1] = TokenCommon.ANDROIDDEVICEID;    //設(shè)備id
    by[2] = (byte) dateLength;  //  數(shù)據(jù)長度
    by[3] = (byte) (~dateLength);   //  數(shù)據(jù)長度取反
    by[4] = TokenCommon.REQUESTGETROBOTDATA; // 命令字:獲取主控信息
    by[5] = ByteUtil.getCheckSum(by);
    try {
        if(null != mOutputStream){
            mOutputStream.write(by);
        } else {
            Log.e(TAG,"串口打開失敗");
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

將自己的指令按照協(xié)議組成一個byte[],然后直接發(fā)送byte數(shù)組

然后我們來驗證發(fā)送是否成功。我們可以將串口連接電腦在電腦上開一個串口調(diào)試助手,然后選擇自己的串口號與波特率


電腦串口調(diào)試助手

我們可以看到接收區(qū)接收到了我們發(fā)送的數(shù)據(jù),則證明發(fā)送是正常的

步驟四:接收串口數(shù)據(jù)

個人覺得串口最蛋疼的就是接收數(shù)據(jù)了,因為他是一個字符一個字符傳過來的,哪怕你的是一條完整的數(shù)據(jù),他也是一個一個字符接收。他不像我們的tcp或udp一樣發(fā)送一條完整數(shù)據(jù)除非過長或網(wǎng)絡(luò)造成的數(shù)據(jù)丟失,我們是能一次性接收一條數(shù)據(jù)的。

而串口一個一個字符接收的,而且還有可能接收的數(shù)據(jù)是亂的,還有可能接收的數(shù)據(jù)是不完整的,還有可能接收到的數(shù)據(jù)在下一條在給你,多么坑啊。好吧既然這樣我們也是有辦法的,這樣就需要我們就必須做數(shù)據(jù)的拼接。

我們先看開源項目的接收源碼

    @Override
protected void onDataReceived(final byte[] buffer, final int size) {
    runOnUiThread(new Runnable() {
        public void run() {
            if (mReception != null) {
                mReception.append(new String(buffer, 0, size));
            }
        }
    });
}

注意1:onDataReceived是實現(xiàn)的一個抽象方法,因為源碼已經(jīng)封裝只要一旦有數(shù)據(jù)就會調(diào)用此方法。在步驟二中可以看到。
注意2:此方法接收數(shù)據(jù)是已字符串接收的,并且每做任何的數(shù)據(jù)處理,只是單純的字符串追加之后顯示數(shù)據(jù),這肯定是無法滿足我們的需求的,因為我們接收的數(shù)據(jù)也很有可能是一個byte數(shù)組。

所以我們自己的自己寫數(shù)據(jù)處理播放。

    @Override
protected void onDataReceived(final byte[] buffer, final int size) {
    //獲取串口傳過來的數(shù)據(jù)
    System.arraycopy(buffer, 0, b, unDisposeLen, size);
    unDisposeLen += + size;
    int temp = 0;  //數(shù)組下標
    while(temp <= unDisposeLen){
        Log.e(TAG, "****************************"+Arrays.toString(b));//字節(jié)數(shù)組打印
        //判斷數(shù)據(jù)長度是否足夠:
        if(unDisposeLen - temp >= FormatToken.DATA_MIN_LEN){
            //分別判斷校驗頭、設(shè)備id、數(shù)據(jù)長度(數(shù)據(jù)長度和數(shù)據(jù)長度校驗判斷是否相等)
            if(b[temp] == FormatToken.head && (b[temp + 1] == FormatToken.deviceId) && b[temp + 2] == ~b[temp + 3]){
                int dataLen = (int)b[temp + 2];
                Log.e(TAG,"數(shù)據(jù)長度="+dataLen);
                //判斷校驗和位數(shù)據(jù)是否相等;接收到的校驗和與發(fā)送的校驗和相等
                //將完整數(shù)據(jù)減掉最后的chesum之后得到一個byte[]
                byte che[] = new byte[FormatToken.DATA_MIN_LEN + dataLen - 1];
                System.arraycopy(b, temp, che, 0, FormatToken.DATA_MIN_LEN + dataLen - 1);
                //將減掉最后的chesum之后得到一個byte[]校驗得到chesum
                byte chesum = ByteUtil.getCheckSum(che); //得到chesum
                //判斷計算出來的chesum與命令中的chesum是否相等
                if(b[temp + dataLen + 5] == chesum){
                    //chesum相等證明為一條完整數(shù)據(jù)
                    byte by[] = new byte[FormatToken.DATA_MIN_LEN + dataLen];
                    //將完整數(shù)據(jù)從總數(shù)據(jù)中copy出來
                    System.arraycopy(b, temp, by, 0, FormatToken.DATA_MIN_LEN + dataLen);
                    Log.e(TAG, "完整數(shù)據(jù)=" + Arrays.toString(by));//字節(jié)數(shù)組打印

                    temp = temp + by.length; //移動下標
                    byte[] ret = new byte[unDisposeLen - temp];
                    //將為處理的數(shù)據(jù)copy到ret數(shù)組中
                    System.arraycopy(b,temp,ret,0,ret.length);
                    //將未處理的數(shù)據(jù)ret替換從0開始替換到
                    System.arraycopy(ret,0,b, 0 ,ret.length);
                    Log.e(TAG, "&&&&&&&&&&&&&&&&&&&&&&&&&&&&" + Arrays.toString(b));//字節(jié)數(shù)組打印
                    unDisposeLen = unDisposeLen - temp; //存儲未完成的數(shù)據(jù)長度
                    temp = 0;

                    if(temp == unDisposeLen) {
                        temp = 0 ;
                        unDisposeLen = 0;
                        break;
                    }
                } else {
                    temp++;
                }
            } else {
                temp++;
            }
        } else {
            break;
        }
    }
    if(temp == unDisposeLen) {
        unDisposeLen = 0;
    }

}

上面的代碼主要是將一個一個字符數(shù)據(jù)根據(jù)你的協(xié)議拼接成一條完整的數(shù)據(jù)。代碼中的注釋已經(jīng)寫的很清楚了。

然后我們驗證一下:用電腦調(diào)式工具發(fā)送內(nèi)容


電腦調(diào)式助手發(fā)送數(shù)據(jù)

然后看android下的數(shù)據(jù)接收

android下的數(shù)據(jù)接收

到此證明數(shù)據(jù)的接收木有問題。

思路是:獲取一段串口數(shù)組數(shù)據(jù),判斷數(shù)據(jù)中是否包含協(xié)議頭和設(shè)備id,如果沒有則繼續(xù)拼接字符串,如果有則判斷數(shù)組的長度是否大于最小協(xié)議長度,如果小于則繼續(xù)追加,如果大于則根據(jù)數(shù)據(jù)長度獲取數(shù)據(jù)部分,然后將數(shù)據(jù)部分加上協(xié)議頭等數(shù)據(jù)組成一個數(shù)組,然后判斷數(shù)組檢驗碼是否相同,如果相等證明是一條完整數(shù)據(jù),并將拼接的數(shù)據(jù)移除掉完整數(shù)據(jù)部分(避免重復(fù)解析已經(jīng)處理過的數(shù)據(jù)。)如果不是則丟棄數(shù)據(jù)。

demo的github地址 https://github.com/SouvDc/SerialPort_project

完結(jié)

OK,到這里我們已經(jīng)能完整解析出數(shù)據(jù),但是一定要根據(jù)你自己的協(xié)議來調(diào)整。


慢慢努力做好身邊所有的事

求知若饑,虛心若愚

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容