1. 程式人生 > >Android 串列埠藍芽通訊開發Java版本

Android 串列埠藍芽通訊開發Java版本

Android串列埠BLE藍芽通訊Java版

0. 導語

我們都知道,在物聯網中,BLE藍芽是通訊裝置的關鍵裝置。在傳統的物聯網應用中,無線WIFI、藍芽和Zigbee,還有一些其他的2.4GHz的無線網絡卡等。對於一個面向物聯網的嵌入式工程師來講,不單單是要學會底層硬體嵌入式或者是嵌入式Linux驅動的開發,還要掌握上層應用,以理解開發流程,方便未來和軟體應用工程師協作開發。“也只有穿別人的鞋子,才知道別人的感覺”似乎就是這個道理吧。

16年的時候,我嘗試做過Qt版本的Android,使用的是C++語言,搭建完環境之後開發出了基於Qt的藍芽通訊demo。雖然Qt在不斷的完善對於Android的支援,但終究C++向Java轉變的時候,效率是一方面的問題,庫同樣是一方面的問題。Qt的定位也只能是臨時應付,或者說應用內的C++演算法繁瑣的時候,選擇C++開發Android才是一個上上策。

對於面向物聯網的嵌入式工程師,對於Android不需要掌握太深刻,重頭戲不在這裡,但是必須要有了解,也要會開發,我猜Android工程師的精髓在於業務邏輯處理上,而對於我們嵌入式工程師來講,我們只在乎功能的實現,也只是Android工程師掌握的一個邊角而已。後繼,我們還會開發基於Java Android的Socket程式設計和伺服器通訊,還有Wifi程式設計。

本文的功能和Qt on Android 藍芽通訊開發一樣,只是使用Java進行重寫。也不得不說,Java的UI比Qt好多了,(在沒有專業的UI製作下),在物聯網通訊中BLE藍芽適合傳輸一些控制命令,或者一些家居資料包括溫度溼度等等。

1. 藍芽通訊組成

我們將使用藍芽功能按順序分成三個部分。第一個部分,藍芽的控制,對於藍芽的開啟和關閉;第二部分,對於藍芽的搜尋,並且可以增加到UI列表裡面;第三部分,藍芽資料傳輸和通訊,可以傳送資料,可以接收資料。如圖所示,為整個藍芽demo區域,實現了最基本的功能,同Qt on Android藍芽通訊開發一樣,有藍芽列表顯示+接收和傳送和藍芽控制的基本功能。下一步就進行藍芽通訊的開發介紹。

1.1 藍芽開發架構

如圖所示,為藍芽Activity的介面,裡面包含了,TextView,ListView,LineEdit,和一些按鈕,使用的是相對佈局,為該藍芽開發提供最基本的功能控制。

XML佈局程式碼如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/tv_bluelist"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="10dp"
            android:text="Bluetooth List:"
            android:textColor="#000"
            android:textSize="15dp"
            />
        <ListView
            android:id="@+id/lv_bluelist"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:paddingTop="1dp"
            android:layout_marginTop="5dp">
        </ListView>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Recv display:"
            android:textColor="#000"
            android:textSize="15dp"
            android:layout_marginTop="5dp"
            />

        <TextView
            android:id="@+id/tv_recv"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            android:layout_marginTop="5dp"
            android:textColor="#000"

            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Send:"
            android:textColor="#000"
            android:textSize="15dp"
            android:layout_marginTop="5dp"
            />
        <EditText
            android:id="@+id/et_send"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginTop="5dp"
            android:textColor="#000"
            android:textSize="12dp"
            />
        <TableLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            >
            <TableRow
                android:layout_width="match_parent"
                android:layout_marginHorizontal="10dp"
                >
                <Button
                    android:id="@+id/btn_send"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:text="Send" />

                <Button
                    android:id="@+id/btn_disconnect"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:text="Disconnect" />

                <Button
                    android:id="@+id/btn_scan"
                    android:layout_width="120dp"
                    android:layout_height="wrap_content"
                    android:text="Scan Device" />
            </TableRow>
            <TableRow
                android:layout_width="match_parent"
                android:layout_marginHorizontal="10dp"
                >
                <Button
                    android:id="@+id/btn_clear"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Clear" />

                <Button
                    android:id="@+id/btn_open"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Open" />

                <Button
                    android:id="@+id/btn_close"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Close" />
            </TableRow>
        </TableLayout>
    </LinearLayout>


</RelativeLayout>

值得注意的是,按鈕使用的是TableLayout進行佈局,裡面請注意觀察TableRow對按鈕進行劃分。

1.2 許可權申請

在Android 6.0 以前,申請藍芽的控制權限只需在mainfest.xml中加入permission專案即可,但是Android 6.0系統需要動態申請許可權,而且使用藍芽搜尋需要申請位置許可權,否則無法申請藍芽的許可權

  • AndroidManifest.xml檔案中
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

分別是藍芽許可權和定位許可權。

  • 在藍芽的activity oncreate中動態申請定位許可權
        /*
        * GPS COARSE LOCATION permission checked.
        *
        * */
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                    MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);

            if(ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.READ_CONTACTS)) {
                Toast.makeText(this, "shouldShowRequestPermissionRationale", Toast.LENGTH_SHORT).show();
            }
        }

在執行藍芽的activity中會執行許可權掃描,確認使用者是否具備定位申請的許可權。

2 藍芽操作

2.1 藍芽的開啟和關閉

在demo中提供了藍芽開啟和關閉的功能,通過控制類來完成對於藍芽的開啟關閉。

import android.bluetooth.BluetoothAdapter;

BluetoothAdapter類就是對藍芽實行開啟和控制的類,包括開啟藍芽,關閉藍芽,檢視藍芽開啟關閉狀態。如下程式碼為開機對於藍芽裝置的開啟和關閉檢測。

        /*
        *  Check bluetooth state.
        * */
        BluetoothAdapter mBluetoothAdapter = new BluetoothAdapter();
        mBluetoothAdapter =   BluetoothAdapter.getDefaultAdapter();
        if( mBluetoothAdapter == null ) {
            Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show();
            finish();
            return;
        }
        if( !mBluetoothAdapter.isEnabled() ) {
            //Toast.makeText(this, "Please enable your Bluetooth and re-run this program.", Toast.LENGTH_LONG).show();
            mBtnOpen.setEnabled(true);
            mBtnClose.setEnabled(false);
            System.out.println("is enable");

        }else{
            mBtnOpen.setEnabled(false);
            mBtnClose.setEnabled(true);
            System.out.println("is not enable");
        }

藍芽的開啟:mBluetoothAdapter.enable();

藍芽的關閉:mBluetoothAdapter.disable();

把他們相應的放在按鈕的點選事件即可。

2.2 藍芽的搜尋

藍芽搜尋的功能需要使用安卓的廣播功能在UI上還要建立一個ListView將搜尋到的周邊藍芽裝置加入到裝置列表,在demo中就是這樣實現的,還能通過ListView的點選事件和該藍芽裝置進行配對連線。

2.2.1 建立Listview列表

新建一個Activity類:

public class BluetoothDeviceAdapter extends BaseAdapter

專門來處理搜尋到的裝置然後加入到Listview列表裡面。

package com.mltbns.root.delvisapp;

import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.ArrayList;


import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

public class BluetoothDeviceAdapter extends BaseAdapter {

    private ArrayList<BluetoothDevice> mLeDevices;
    //LayoutInflater是用來找res/layout/下的xml佈局檔案,並且例項化
    //它的作用類似於findViewById()
    private LayoutInflater mInflator;
    private Activity mContext;//獲得 LayoutInflater 例項的一種方法就是使用Activity;

    public BluetoothDeviceAdapter(Activity c) {
        super();
        mContext = c;
        mLeDevices = new ArrayList<BluetoothDevice>();
        mInflator = mContext.getLayoutInflater();
    }


    public void addDevice(BluetoothDevice device) {
        if (!mLeDevices.contains(device)) {
            mLeDevices.add(device);
            System.out.println(device.getName() + "  " + device.getAddress());
        }
    }

    // 獲取子項中對應的裝置
    public BluetoothDevice getDevice(int position) {
        return mLeDevices.get(position);
    }

    // 清空列表的資料
    public void clear() {
        mLeDevices.clear();
    }

    @Override
    public int getCount() {
        return mLeDevices.size();
    }

    @Override
    public Object getItem(int position) {
        return mLeDevices.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View view, ViewGroup parent) {
        ViewHolder viewHolder;
        // General ListView optimization code.
        if (view == null) {
            view = mInflator.inflate(R.layout.activity_bluetooth_device_adapter, null);//例項化這個控制元件
            viewHolder = new ViewHolder();
            viewHolder.deviceAddress = (TextView) view.findViewById(R.id.Address);
            viewHolder.deviceName = (TextView) view.findViewById(R.id.Name);
            view.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) view.getTag();
            //the Object stored in this view as a tag
        }

        // 對應的裝置進行處理
        BluetoothDevice device = mLeDevices.get(position);
        final String deviceName = device.getName();
        if (deviceName != null && deviceName.length() > 0) {
            viewHolder.deviceName.setText(deviceName);
        } else {
            viewHolder.deviceName.setText("未知裝置");
        }
        if( device.getBondState() == BluetoothDevice.BOND_BONDED ) {
            viewHolder.deviceName.setTextColor(Color.rgb(75,0,130));

        }else{
            viewHolder.deviceName.setTextColor(Color.rgb(47,79,79));
        }

        viewHolder.deviceAddress.setText(device.getAddress());



        return view;
    }

    final class ViewHolder {
        TextView deviceName;
        TextView deviceAddress;
    }

}

2.2.2 建立BluetoothReceiver

public class BluetoothReceiver extends BroadcastReceiver {}

該類整合BroadcastReceiver類,裡面有個onReceive的事件,當搜尋到藍芽的時候,就會將該藍芽的裝置資訊傳送進來。

    public class BluetoothReceiver extends BroadcastReceiver {


        private String pair_info;
        private String unpair_info;
        private String state_info;


        @Override
        public void onReceive(Context context, Intent intent ) {

            String action = intent.getAction();
            System.out.println ( "SYSTEM: action triggered: " + action  );

            if(BluetoothDevice.ACTION_FOUND.equals(action)) {

                BluetoothDevice device = intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE );
                mLvDeviceList.setAdapter(mBluetoothDeviceAdapter);
                System.out.println ( "SYSTEM: Find a device : " + device.getName() + " : " + device.getAddress()  );
                // Scanned a device add to List
                mBluetoothDeviceAdapter.addDevice(device);
                // 資料改變並更新列表
                mBluetoothDeviceAdapter.notifyDataSetChanged();
                if( device.getBondState() == BluetoothDevice.BOND_BONDED ) {
                    pair_info = device.getAddress();
                }else {
                    unpair_info = device.getAddress();
                }

            }else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){
                System.out.println ( "SYSTEM: Discovery finished..."  );
            }
        }

        public void set_pairInfo( String val ) {
            pair_info = val;
        }
        public String get_pairInfo() {
            return pair_info;
        }
        public void set_unpairInfo( String val ) {
            unpair_info = val;
        }
        public String get_unpairInfo() {
            return unpair_info;
        }
        public void set_stateInfo( String val ) {
            state_info = val;
        }
        public String get_stateInfo() {
            return state_info;
        }
    }

當搜尋到了資訊之後就將該裝置傳入BluetoothDeviceAdapter中,然後該裝置的資訊都顯示。搜尋到的資訊調入這個方法mBluetoothDeviceAdapter.addDevice(device);就完成了裝置的新增,然後重新整理顯示:mBluetoothDeviceAdapter.notifyDataSetChanged();這樣就完成了最終的顯示。

使用mBluetoothAdapter.startDiscovery();開始對藍芽進行查詢。

如圖所示,上面顯示的是藍芽的名字,下面顯示的MAC地址,對於已經配對的裝置可以用if( device.getBondState() == BluetoothDevice.BOND_BONDED )來進行判斷,然後設定已配對藍芽的名字為更鮮豔的顏色區分未配對裝置。

2.3 連線裝置

本demo在listview增加點選事件,當點選裝置的時候激發對於該device的連線。這個連線的過程我們需要新建立一個執行緒進行連線,連線成功之後使用回撥函式的方法通知主執行緒已經連線了,可以進行操作了。在主執行緒(我們的藍芽主activity中新建handler函式專門處理其他執行緒回撥資訊的)

2.3.1 constant常量類

相當於C++的#include 裡面定義一些通訊協議的常量,我們在主執行緒和一會兒建立的搜尋執行緒中會使用該資訊。

public class Constant {
    public static final String CONNECTTION_UUID = "00001101-0000-1000-8000-00805F9B34FB";

    /**
     * 開始監聽
     */

    public static final int MSG_START_LISTENING = 1;

    /**
     * 結束監聽
     */
    public static final int MSG_FINISH_LISTENING = 2;

    /**
     * 有客戶端連線
     */
    public static final int MSG_GOT_A_CLINET = 3;

    /**
     * 連線到伺服器
     */
    public static final int MSG_CONNECTED_TO_SERVER = 4;

    /**
     * 獲取到資料
     */
    public static final int MSG_GOT_DATA = 5;

    /**
     * 出錯
     */
    public static final int MSG_ERROR = -1;
}

2.3.2 connect執行緒

package com.mltbns.root.delvisapp;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;

import java.io.IOException;
import java.util.UUID;

/**
 * 客戶端連線執行緒
 */
public class ConnectThread extends Thread {
    private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
    private BluetoothAdapter mBluetoothAdapter;
    private final Handler mHandler;
    private ConnectedThread mConnectedThread;

    public ConnectThread(BluetoothDevice device, BluetoothAdapter adapter, Handler handler) {
        BluetoothSocket tmp = null;
        mmDevice = device;
        mBluetoothAdapter = adapter;
        mHandler = handler;
        //根據給定的裝置獲取一個BluetoothSocket物件
        try {
            // 和伺服器端使用相同的UUID
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }

    public void run() {
        // 取消搜尋裝置因為會關閉連線
        System.out.println(" Connect +: "+ mmDevice.getName());
        mBluetoothAdapter.cancelDiscovery();
        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.connect();
            System.out.println("Connecting.... ");
        } catch (Exception connectException) {
            mHandler.sendMessage(mHandler.obtainMessage(Constant.MSG_ERROR, connectException));
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }

        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }

    private void manageConnectedSocket(BluetoothSocket mmSocket) {
        mHandler.sendEmptyMessage(Constant.MSG_CONNECTED_TO_SERVER);
        mConnectedThread = new ConnectedThread(mmSocket, mHandler);
        mConnectedThread.start();
        System.out.println("Connected thread start... ");
    }

    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }

    public void sendData(byte[] data) {
        if( mConnectedThread!=null){
            mConnectedThread.write(data);
        }
    }
}

我們在對listview裡面有點選事件的函式,點選的專案索引編號和device的資訊進行了繫結,然後進行了連線。通過該執行緒就完成了對於藍芽裝置的連線。

2.4 藍芽資料傳輸

2.4.1 connected執行緒

連線完成之後,進入的執行緒,主要是進行收發訊息的。

package com.mltbns.root.delvisapp;

import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Created by Rex on 2015/5/30.
 */
public class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;
    private final Handler mHandler;

    public ConnectedThread(BluetoothSocket socket, Handler handler) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        mHandler = handler;
        // 獲取輸入輸出流
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
    public void run() {
        byte[] buffer = new byte[1024];  // buffer store for the stream
        int bytes;
        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // 從輸入流讀取資料
                bytes = mmInStream.read(buffer);
                // Send the obtained bytes to the UI activity
                if( bytes >0) {
                    Message message = mHandler.obtainMessage(Constant.MSG_GOT_DATA, new String(buffer, 0, bytes, "utf-8"));
                    mHandler.sendMessage(message);
                }
                Log.d("GOTMSG", "message size" + bytes);
            } catch (IOException e) {
                mHandler.sendMessage(mHandler.obtainMessage(Constant.MSG_ERROR, e));
                break;
            }
        }
    }
    /* 傳送資料島遠端裝置*/
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
    /* 關閉連線 */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

2.4.2 主執行緒的handler

private void showToast(String text) {

    if( mToast == null) {
        mToast = Toast.makeText(this, text, Toast.LENGTH_LONG);
    }
    else {
        mToast.setText(text);
    }
    mToast.show();
}
/**
 * 處理訊息
 */
private class MyHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case Constant.MSG_START_LISTENING:
                setProgressBarIndeterminateVisibility(true);
                System.out.println("Start to listener...");
                break;
            case Constant.MSG_FINISH_LISTENING:
                setProgressBarIndeterminateVisibility(false);
                System.out.println("stop listenner");
                break;
            case Constant.MSG_GOT_DATA:
                mTextView.append(String.valueOf(msg.obj));
                System.out.println("data: "+String.valueOf(msg.obj));
                break;
            case Constant.MSG_ERROR:
                System.out.println("error: "+String.valueOf(msg.obj));
                break;
            case Constant.MSG_CONNECTED_TO_SERVER:
                System.out.println("Connected to Server");
                mLvDeviceList.setEnabled(false);
                mLvDeviceList.setBackgroundColor(Color.rgb(119,136,153));
                showToast("Bluetooth connection has been set up!");
                break;
            case Constant.MSG_GOT_A_CLINET:
                System.out.println("Got a Client");
                break;
        }
    }
}

在開始搜尋裝置執行緒的時候我們將handler也傳遞給了執行緒,執行緒在執行完搜尋完成之後就會進入handler的函式按照回撥資訊進入case裡面,輸出相關資訊。

這裡有收到資訊,資訊錯誤,連線成功的事件,我們在相應的事件裡面進行進行執行函式就好了。

2.4.3 函式的傳送

String text = mEditText.getText().toString();
connectThread.sendData( text.getBytes() );

我們獲取editText元件的字串,然後用connectThread的send函式就能將資料傳送出去。

2.4.4 函式的接收

mTextView.append(String.valueOf(msg.obj));
System.out.println("data: "+String.valueOf(msg.obj));

該函式在handler函式體內,是case Constant.MSG_GOT_DATA:裡面的。

3 原始碼下載

4 參考文獻

[2] qq_22252423, Android串列埠藍芽開發實戰
, csdn, 2017年07月18日

[5] Small_Lee, 藍芽實戰(三)
, csdn, 2016年3月15日