【Android】藍牙開發——經典藍牙(附Demo源碼)

目錄

一、前言

二、經典藍牙的介紹

三、經典藍牙項目實戰

四、Demo案例源碼地址:

 

一、前言

去年畢業進入公司以來,工作內容主要和藍牙打交道,幾個月的學習和實踐讓我這個Android藍牙小白逐漸成長起來。但是,很多時候知識溫故才能知新,每一次實踐都會帶來新的理解和體會。於是決定從今天開始,將這幾個月以來的成長在博客中一一分享出來,給有需要的朋友作些參考,也歡迎大家提出指點和建議。

二、經典藍牙的介紹

關於經典藍牙的介紹,google官網上有詳細的解釋,此處貼上鍊接:https://developer.android.google.cn/guide/topics/connectivity/bluetooth

經典藍牙的使用過程大致可分爲以下幾個步驟:

1、開啓掃描,搜索周圍藍牙設備

2、掃描到設備後,與設備配對、建立連接

3、與設備成功連接後,實現數據通訊即收發數據

4、與設備通訊結束後,關閉與藍牙的連接

三、經典藍牙項目實戰

1、在開始使用藍牙之前,我們必須要聲明兩個權限,第一個是藍牙權限,第二個是位置權限。

(1)藍牙權限

    <!-- 應用使用藍牙的權限 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <!--啓動設備發現或操作藍牙設置的權限-->
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

(2)位置權限(注意:Android 6.0以上版本還需要動態申請位置權限!)

<!--位置權限-->
<!--Android 10以上系統,需要ACCESS_FINE_LOCATION-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--Android 9以及以下系統,需要ACCESS_FINE_LOCATION-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

2、創建發起藍牙連接的線程ConnectThread.java 和 管理藍牙連接、收發數據的線程ConnectedThread.java。

(1)ConnectThread.java,代碼中已經有詳細的註釋。

package yc.bluetooth.androidbt;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;

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


/**
 * 發起藍牙連接
 */
public class ConnectThread extends Thread {
    private static final String TAG = "ConnectThread";
    private final BluetoothAdapter mBluetoothAdapter;
    private BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothAdapter bluetoothAdapter,BluetoothDevice bluetoothDevice,String uuid)  {
       this.mBluetoothAdapter = bluetoothAdapter;
       this.mmDevice = bluetoothDevice;

       //使用一個臨時變量,等會賦值給mmSocket
       //因爲mmSocket是靜態的
       BluetoothSocket tmp = null ;

       if(mmSocket != null){
           Log.e(TAG,"ConnectThread-->mmSocket != null先去釋放");
           try {
               mmSocket.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
        Log.d(TAG,"ConnectThread-->mmSocket != null已釋放");

        //1、獲取BluetoothSocket
        try {
            //建立安全的藍牙連接,會彈出配對框
            tmp = mmDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid));

        } catch (IOException e) {
           Log.e(TAG,"ConnectThread-->獲取BluetoothSocket異常!" + e.getMessage());
        }

        mmSocket = tmp;
        if(mmSocket != null){
            Log.w(TAG,"ConnectThread-->已獲取BluetoothSocket");
        }

    }

    @Override
    public void run(){

        //連接之前先取消發現設備,否則會大幅降低連接嘗試的速度,並增加連接失敗的可能性
        if(mBluetoothAdapter == null){
            Log.e(TAG,"ConnectThread:run-->mBluetoothAdapter == null");
            return;
        }
        //取消發現設備
        if(mBluetoothAdapter.isDiscovering()){
            mBluetoothAdapter.cancelDiscovery();
        }

        if(mmSocket == null){
            Log.e(TAG,"ConnectThread:run-->mmSocket == null");
            return;
        }

        //2、通過socket去連接設備
        try {
            Log.d(TAG,"ConnectThread:run-->去連接...");
            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onStartConn();  //開始去連接回調
            }
            mmSocket.connect();  //connect()爲阻塞調用,連接失敗或 connect() 方法超時(大約 12 秒之後),它將會引發異常

            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onConnSuccess(mmSocket);  //連接成功回調
                Log.w(TAG,"ConnectThread:run-->連接成功");
            }

        } catch (IOException e) {
            Log.e(TAG,"ConnectThread:run-->連接異常!" + e.getMessage());

            if(onBluetoothConnectListener != null){
                onBluetoothConnectListener.onConnFailure("連接異常:" + e.getMessage());
            }

            //釋放
            cancel();
        }

    }

    /**
     * 釋放
     */
    public void cancel() {
        try {
            if (mmSocket != null && mmSocket.isConnected()) {
                Log.d(TAG,"ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected());
                mmSocket.close();
                mmSocket = null;
                return;
            }

            if (mmSocket != null) {
                mmSocket.close();
                mmSocket = null;
            }

            Log.d(TAG,"ConnectThread:cancel-->關閉已連接的套接字釋放資源");

        } catch (IOException e) {
            Log.e(TAG,"ConnectThread:cancel-->關閉已連接的套接字釋放資源異常!" + e.getMessage());
        }
    }

    private OnBluetoothConnectListener onBluetoothConnectListener;

    public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) {
        this.onBluetoothConnectListener = onBluetoothConnectListener;
    }

    //連接狀態監聽者
    public interface OnBluetoothConnectListener{
        void onStartConn();  //開始連接
        void onConnSuccess(BluetoothSocket bluetoothSocket);  //連接成功
        void onConnFailure(String errorMsg);  //連接失敗
    }

}

(2)ConnectedThread.java,代碼中已經有詳細的註釋。

package yc.bluetooth.androidbt;

import android.bluetooth.BluetoothSocket;
import android.util.Log;

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


/**
 * 管理連接
 * 1、發送數據
 * 2、接收數據
 */
public class ConnectedThread extends Thread{
    private static final String TAG = "ConnectedThread";
    private BluetoothSocket mmSocket;
    private InputStream mmInStream;
    private OutputStream mmOutStream;
    //是否是主動斷開
    private boolean isStop = false;
    //發起藍牙連接的線程
    private ConnectThread connectThread;

    public void terminalClose(ConnectThread connectThread){
        isStop = true;
        this.connectThread = connectThread;
    }

    public ConnectedThread(BluetoothSocket socket){
        mmSocket = socket;

        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        //使用臨時對象獲取輸入和輸出流,因爲成員流是靜態類型

        //1、獲取 InputStream 和 OutputStream
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();

        } catch (IOException e) {
            Log.e(TAG,"ConnectedThread-->獲取InputStream 和 OutputStream異常!");
        }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;

        if(mmInStream != null){
            Log.d(TAG,"ConnectedThread-->已獲取InputStream");
        }

        if(mmOutStream != null){
            Log.d(TAG,"ConnectedThread-->已獲取OutputStream");
        }

    }

    public void run(){
        //最大緩存區 存放流
        byte[] buffer = new byte[1024 * 2];  //buffer store for the stream
        //從流的read()方法中讀取的字節數
        int bytes = 0;  //bytes returned from read()

        //持續監聽輸入流直到發生異常
        while(!isStop){
            try {

                if(mmInStream == null){
                    Log.e(TAG,"ConnectedThread:run-->輸入流mmInStream == null");
                    break;
                }
                //先判斷是否有數據,有數據再讀取
                if(mmInStream.available() != 0){
                    //2、接收數據
                    bytes = mmInStream.read(buffer);  //從(mmInStream)輸入流中(讀取內容)讀取的一定數量字節數,並將它們存儲到緩衝區buffer數組中,bytes爲實際讀取的字節數
                    byte[] b = Arrays.copyOf(buffer,bytes);  //存放實際讀取的數據內容
                    Log.w(TAG,"ConnectedThread:run-->收到消息,長度" + b.length + "->" + bytes2HexString(b, b.length));  //有空格的16進制字符串
                    if(onSendReceiveDataListener != null){
                        onSendReceiveDataListener.onReceiveDataSuccess(b);  //成功收到消息
                    }
                }

            } catch (IOException e) {
                Log.e(TAG,"ConnectedThread:run-->接收消息異常!" + e.getMessage());
                if(onSendReceiveDataListener != null){
                    onSendReceiveDataListener.onReceiveDataError("接收消息異常:" + e.getMessage());  //接收消息異常
                }
                //關閉流和socket
                boolean isClose = cancel();
                if(isClose){
                    Log.e(TAG,"ConnectedThread:run-->接收消息異常,成功斷開連接!");
                }
                break;
            }
        }
        //關閉流和socket
        boolean isClose = cancel();
        if(isClose){
            Log.d(TAG,"ConnectedThread:run-->接收消息結束,斷開連接!");
        }
    }

    //發送數據
    public boolean write(byte[] bytes){
        try {

            if(mmOutStream == null){
                Log.e(TAG, "mmOutStream == null");
                return false;
            }

            //發送數據
            mmOutStream.write(bytes);
            Log.d(TAG, "寫入成功:"+ bytes2HexString(bytes, bytes.length));
            if(onSendReceiveDataListener != null){
                onSendReceiveDataListener.onSendDataSuccess(bytes);  //發送數據成功回調
            }
            return true;

        } catch (IOException e) {
            Log.e(TAG, "寫入失敗:"+ bytes2HexString(bytes, bytes.length));
            if(onSendReceiveDataListener != null){
                onSendReceiveDataListener.onSendDataError(bytes,"寫入失敗");  //發送數據失敗回調
            }
            return false;
        }
    }

    /**
     * 釋放
     * @return   true 斷開成功  false 斷開失敗
     */
    public boolean cancel(){
        try {
            if(mmInStream != null){
                mmInStream.close();  //關閉輸入流
            }
            if(mmOutStream != null){
                mmOutStream.close();  //關閉輸出流
            }
            if(mmSocket != null){
                mmSocket.close();   //關閉socket
            }
            if(connectThread != null){
                connectThread.cancel();
            }

            connectThread = null;
            mmInStream = null;
            mmOutStream = null;
            mmSocket = null;

            Log.w(TAG,"ConnectedThread:cancel-->成功斷開連接");
            return true;

        } catch (IOException e) {
            // 任何一部分報錯,都將強制關閉socket連接
            mmInStream = null;
            mmOutStream = null;
            mmSocket = null;

            Log.e(TAG, "ConnectedThread:cancel-->斷開連接異常!" + e.getMessage());
            return false;
        }
    }

    /**
     * 字節數組-->16進制字符串
     * @param b   字節數組
     * @param length  字節數組長度
     * @return 16進制字符串 有空格類似“0A D5 CD 8F BD E5 F8”
     */
    public static String bytes2HexString(byte[] b, int length) {
        StringBuffer result = new StringBuffer();
        String hex;
        for (int i = 0; i < length; i++) {
            hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            result.append(hex.toUpperCase()).append(" ");
        }
        return result.toString();
    }

    private OnSendReceiveDataListener onSendReceiveDataListener;

    public void setOnSendReceiveDataListener(OnSendReceiveDataListener onSendReceiveDataListener) {
        this.onSendReceiveDataListener = onSendReceiveDataListener;
    }

    //收發數據監聽者
    public interface OnSendReceiveDataListener{
        void onSendDataSuccess(byte[] data);  //發送數據結束
        void onSendDataError(byte[] data, String errorMsg); //發送數據出錯
        void onReceiveDataSuccess(byte[] buffer);  //接收到數據
        void onReceiveDataError(String errorMsg);   //接收數據出錯
    }



}

3、使用藍牙之前,首先要檢查當前手機是否支持藍牙。如果支持藍牙,檢查手機藍牙是否已開啓。如果沒有開啓,則需要先打開藍牙。打開手機藍牙,有兩種方式,推薦使用第二種打開方式。

private void initBluetooth() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if(bluetoothAdapter == null){
            Toast.makeText(this, "當前手機設備不支持藍牙", Toast.LENGTH_SHORT).show();
        }else{
            //手機設備支持藍牙,判斷藍牙是否已開啓
            if(bluetoothAdapter.isEnabled()){
                Toast.makeText(this, "手機藍牙已開啓", Toast.LENGTH_SHORT).show();
            }else{
                //藍牙沒有打開,去打開藍牙。推薦使用第二種打開藍牙方式
                //第一種方式:直接打開手機藍牙,沒有任何提示
//                bluetoothAdapter.enable();  //BLUETOOTH_ADMIN權限
                //第二種方式:友好提示用戶打開藍牙
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivity(enableBtIntent);
            }
        }
    }

4、確保手機藍牙已打開,就可以開始搜索設備。搜索設備只需調用startDiscovery()方法,但搜索的結果是通過廣播來獲取的,所以,還需要定義廣播來獲取搜索到的設備。

(1)搜索設備

private void searchBtDevice() {
        if(bluetoothAdapter.isDiscovering()){ //當前正在搜索設備...
            return;
        }
        //開始搜索
        bluetoothAdapter.startDiscovery();
    }

(2)自定義廣播接收器,接收搜索到的設備。

/**
     * 藍牙廣播接收器
     */
    private static class BtBroadcastReceiver extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) { //開啓搜索
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDiscoveryStart();  //開啓搜索回調
                }

            } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {//完成搜素
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDiscoveryStop();  //完成搜素回調
                }

            } else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) {  //3.0搜索到設備
                //藍牙設備
                BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                //信號強度
                int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);

                Log.d(TAG, "掃描到設備:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
                if (onDeviceSearchListener != null) {
                    onDeviceSearchListener.onDeviceFound(bluetoothDevice,rssi);  //3.0搜素到設備回調
                }
            }
        }

        /**
         * 藍牙設備搜索監聽者
         * 1、開啓搜索
         * 2、完成搜索
         * 3、搜索到設備
         */
        public interface OnDeviceSearchListener {
            void onDiscoveryStart();   //開啓搜索
            void onDiscoveryStop();    //完成搜索
            void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi);  //搜索到設備
        }

        private OnDeviceSearchListener onDeviceSearchListener;

        public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) {
            this.onDeviceSearchListener = onDeviceSearchListener;
        }
    }

(3)註冊廣播接收器

private void initBtBroadcast() {
        //註冊廣播接收
        btBroadcastReceiver = new BtBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //開始掃描
        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//掃描結束
        intentFilter.addAction(BluetoothDevice.ACTION_FOUND);//搜索到設備
        registerReceiver(btBroadcastReceiver,intentFilter);

    }

(4)註冊過廣播之後,要記得在onDestroy()中註銷廣播

@Override
    protected void onDestroy() {
        super.onDestroy();

        //註銷廣播接收
        unregisterReceiver(btBroadcastReceiver);
    }

5、搜索到目標設備之後,就可以與藍牙設備建立連接。

(1)發起連接

 /**
     * 開始連接設備
     * @param bluetoothDevice   藍牙設備
     * @param uuid               發起連接的UUID
     * @param conOutTime        連接超時時間
     */
    public void startConnectDevice(final BluetoothDevice bluetoothDevice, String uuid, long conOutTime){
        if(bluetoothDevice == null){
            Log.e(TAG,"startConnectDevice-->bluetoothDevice == null");
            return;
        }
        if(bluetoothAdapter == null){
            Log.e(TAG,"startConnectDevice-->bluetooth3Adapter == null");
            return;
        }
        //發起連接
        connectThread = new ConnectThread(bluetoothAdapter,curBluetoothDevice,uuid);
        connectThread.setOnBluetoothConnectListener(new ConnectThread.OnBluetoothConnectListener() {
            @Override
            public void onStartConn() {
                Log.d(TAG,"startConnectDevice-->開始連接..." + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress());
            }


            @Override
            public void onConnSuccess(BluetoothSocket bluetoothSocket) {
                //移除連接超時
                mHandler.removeCallbacks(connectOuttimeRunnable);
                Log.d(TAG,"startConnectDevice-->移除連接超時");
                Log.w(TAG,"startConnectDevice-->連接成功");

                Message message = new Message();
                message.what = CONNECT_SUCCESS;
                mHandler.sendMessage(message);

                //標記當前連接狀態爲true
                curConnState = true;
                //管理連接,收發數據
                managerConnectSendReceiveData(bluetoothSocket);
            }

            @Override
            public void onConnFailure(String errorMsg) {
                Log.e(TAG,"startConnectDevice-->" + errorMsg);

                Message message = new Message();
                message.what = CONNECT_FAILURE;
                mHandler.sendMessage(message);

                //標記當前連接狀態爲false
                curConnState = false;

                //斷開管理連接
                clearConnectedThread();
            }
        });

        connectThread.start();
        //設置連接超時時間
        mHandler.postDelayed(connectOuttimeRunnable,conOutTime);

    }

    //連接超時
    private Runnable connectOuttimeRunnable = new Runnable() {
        @Override
        public void run() {
            Log.e(TAG,"startConnectDevice-->連接超時" );

            Message message = new Message();
            message.what = CONNECT_FAILURE;
            mHandler.sendMessage(message);

            //標記當前連接狀態爲false
            curConnState = false;
            //斷開管理連接
            clearConnectedThread();
        }
    };

(2)管理連接,數據收發

 managerConnectSendReceiveData()方法中,connectedThread對象進行數據發送結果、接收結果監聽。

/**
     * 管理已建立的連接,收發數據
     * @param bluetoothSocket   已建立的連接
     */
    public void managerConnectSendReceiveData(BluetoothSocket bluetoothSocket){
        //管理已有連接
        connectedThread = new ConnectedThread(bluetoothSocket);
        connectedThread.start();
        connectedThread.setOnSendReceiveDataListener(new ConnectedThread.OnSendReceiveDataListener() {
            @Override
            public void onSendDataSuccess(byte[] data) {
                Log.w(TAG,"發送數據成功,長度" + data.length + "->" + bytes2HexString(data,data.length));
                Message message = new Message();
                message.what = SEND_SUCCESS;
                message.obj = "發送數據成功,長度" + data.length + "->" + bytes2HexString(data,data.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onSendDataError(byte[] data,String errorMsg) {
                Log.e(TAG,"發送數據出錯,長度" + data.length + "->" + bytes2HexString(data,data.length));
                Message message = new Message();
                message.what = SEND_FAILURE;
                message.obj = "發送數據出錯,長度" + data.length + "->" + bytes2HexString(data,data.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onReceiveDataSuccess(byte[] buffer) {
                Log.w(TAG,"成功接收數據,長度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length));
                Message message = new Message();
                message.what = RECEIVE_SUCCESS;
                message.obj = "成功接收數據,長度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length);
                mHandler.sendMessage(message);
            }

            @Override
            public void onReceiveDataError(String errorMsg) {
                Log.e(TAG,"接收數據出錯:" + errorMsg);
                Message message = new Message();
                message.what = RECEIVE_FAILURE;
                message.obj = "接收數據出錯:" + errorMsg;
                mHandler.sendMessage(message);
            }
        });
    }

 sendData()方法中,connectedThread對象發送數據。

 /**
     * 發送數據
     * @param data      要發送的數據 字符串
     * @param isHex     是否是16進制字符串
     * @return   true 發送成功  false 發送失敗
     */
    public boolean sendData(String data,boolean isHex){
        if(connectedThread == null){
            Log.e(TAG,"sendData:string -->connectedThread == null");
            return false;
        }
        if(data == null || data.length() == 0){
            Log.e(TAG,"sendData:string-->要發送的數據爲空");
            return false;
        }

        if(isHex){  //是16進制字符串
            data.replace(" ","");  //取消空格
            //檢查16進制數據是否合法
            if(data.length() % 2 != 0){
                //不合法,最後一位自動填充0
                String lasts = "0" + data.charAt(data.length() - 1);
                data = data.substring(0,data.length() - 2) + lasts;
            }
            Log.d(TAG,"sendData:string -->準備寫入:" + data);  //加空格顯示
            return connectedThread.write(hexString2Bytes(data));
        }

        //普通字符串
        Log.d(TAG,"sendData:string -->準備寫入:" + data);
        return connectedThread.write(data.getBytes());
    }

6、與藍牙設備通訊結束之後,可與藍牙設備斷開連接。

/**
     * 斷開已有的連接
     */
    public void clearConnectedThread(){
        Log.d(TAG,"clearConnectedThread-->即將斷開");

        //connectedThread斷開已有連接
        if(connectedThread == null){
            Log.e(TAG,"clearConnectedThread-->connectedThread == null");
            return;
        }
        connectedThread.terminalClose(connectThread);

        //等待線程運行完後再斷開
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                connectedThread.cancel();  //釋放連接

                connectedThread = null;
            }
        },10);

        Log.w(TAG,"clearConnectedThread-->成功斷開連接");
        Message message = new Message();
        message.what = DISCONNECT_SUCCESS;
        mHandler.sendMessage(message);

    }

7、項目演示

(1)掃描到設備,點擊“連接”按鈕, 會在“搜索”按鈕下方顯示連接結果 。注意經典藍牙連接是,第一次連接時會有彈出一個配對框,這個具體配對方式是藍牙設備開發人員設置的。

(2)手機給藍牙設備(設備名爲:BTyqy)發送數據成功之後,藍牙設備把接收到的數據再回發送給手機。

(3)斷開連接。點擊“斷開”按鈕, 會在“搜索”按鈕下方顯示斷開結果 。

四、Demo案例源碼地址:

注意:源碼中沒有進行位置權限的靜態聲明以及動態申請,小夥伴們使用時需要自己添加,謝謝!

CSDN:https://download.csdn.net/download/qq_38950819/11615060

碼雲:https://gitee.com/lilium_foliage/Android-Bluetooth

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章