藍牙開發(一)----- 基於藍牙Ble的Android應用開發

前言

    以前一直忙於不斷的做項目,總是覺得沒有必要抽時間出來寫博客,但看到業界的很多大牛都有堅持寫博客的習慣,自己也應該向大佬們看齊纔有機會成爲大牛。其實堅持寫博客的好處還是不少的,一方可以作爲自己對所學知識的一個總結歸納,另外一個方面可以作爲和其他人的一個技術分享交流,好處多多。但萬事開頭難,希望自己能堅持下來,keep moving!

    之前也做過幾個Ble的項目,現在來對這一塊做一個簡單的介紹和總結。

傳統藍牙 VS Ble藍牙

    在Android中藍牙根據協議主要分爲兩種:一種是基於spp協議的傳統藍牙或者叫經典藍牙,另外一種就是Android 4.3才引進的基於GATT協議的BLE(Bluetooth Low Energy)低功耗藍牙,相比較於傳統藍牙其主要優點就是功耗低,搜索、連接速度相對較快,那是不是說相較於傳統藍牙它就沒有缺點呢?答案是否定的,相較於傳統藍牙而言,Ble的數據傳輸效率低,每次可以傳輸的數據量小得多,單次最多隻能傳輸20個byte,而傳統藍牙單次可傳輸的數據量遠不止於此,但也正是由於功耗低這一主要優點,使得現在ble被廣泛應用於智能穿戴設備、智能鎖、心率測量儀等領域 (本文主要介紹的是ble藍牙,若要了解spp協議藍牙的相關開發,可以查看我我另一篇博客藍牙開發(二)----- 基於SPP藍牙協議的Android應用開發)。

cosplay 角色扮演

    在BLE協議中,有兩個角色,周邊(Periphery)和中央(Central)。周邊是數據的提供者,中央是數據的使用和處理者。在Android SDK裏面,Android4.3以後手機只能作爲中央設備使用,直到Android5.0以後手機纔可以作爲周邊設備使用,即此時的手機既可以作爲BLE周邊設備來爲中央提供數據,也可以作爲中央設備接收處理周邊其它藍牙設備傳遞過來的數據進行處理。 一箇中央設備可以同時連接多個周邊設備,但一個周邊設備某一時刻只能連接一箇中央設備。

一些基本概念

  • GATT協議
        我們已經知道了Ble是基於GATT協議的,那麼什麼是GATT協議呢?下面就來簡單介紹一下:
        GATT是Generic Attributes的縮寫,也就是通用屬性的意思,它是ble低功耗藍牙設備之間的標準通信協議。根據藍牙技術官網的介紹,GATT協議定義了一個分層數據結構,該結構暴露給連接的Ble設備進行通信。其數據分層結構如下圖所示:
    在這裏插入圖片描述
GATT協議數據結構圖

    簡單描述一下結構圖的意思就是:Profile是一組服務(Service)的集合,這個文件包含廣播的種類、所使用的連接間隔、所需的安全等級等配置信息,每個Service下面可以包含多個特徵(characteristic),每個特徵包含屬性(properties)和值(value),還可以包含多個描述(descriptor)。
  • Service
    服務是特徵和與其它服​​務關係的集合。每個Service包含一個或多個characteristic,每個Service由一個唯一的UUID標識,UUID可以分爲兩種:一種是經過官方認證的16位UUID,另外一種是由開發者自己定義的128位的UUID,類似於0x0000xxxx-0000-1000-8000-00805F9B34FB,這是藍牙技術聯盟定義的一個基本格式,藍牙模塊的開發者通常只要定義xxxx的部分即可,開發時通常由硬件或嵌入式工程師會告訴我們該模塊包含哪些Service及其對應的UUID。

  • Characteristic
    特徵被定義爲包含單個值的屬性類型。和Service一樣,Characteristic也是由一個UUID進行標識,通常也是由硬件或嵌入式工程師提供。Characteristic是我們進行數據通信的一個重要載體,我們的數據讀、寫、通知等都通過這個類來實現,是比較重要的一個類。

    這裏就只介紹Service和Characteristic這兩個比較重要的數據結構,Characteristic中的屬性和描述等就不一一介紹了,有興趣的可以去查閱相關資料。

Ble開發的幾個步驟

準備

從硬件或嵌入式工程師手上拿到需要的UUID:

	public static final UUID UUID_SERVICE = UUID.fromString("0000fff2-0000-1000-8000-00805f9b34fb");  //主Service的UUID
    public static final UUID UUID_NOTIFY = UUID.fromString("0000fff3-0000-1000-8000-00805f9b34fb"); //具有通知屬性的UUID
    public static final UUID UUID_READ   = UUID.fromString("0000fff5-0000-1000-8000-00805f9b34fb"); //具有讀取屬性的UUID
    public static final UUID UUID_WRITE  = UUID.fromString("0000fff7-0000-1000-8000-00805f9b34fb"); //具有寫入屬性的UUID
第一步 配置清單文件
	<uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
   <!-- Android6.0及以上必須獲取位置權限,否則無法掃描到周邊的藍牙設備  --> 
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
第二步 檢查設備,獲取BluetoothAdapter

進行這步之前記得先進行關於定位權限的動態適配

	/**
 	* 判斷該設備是否支持Ble並獲取BluetoothAdapter
 	*/
	public Boolean ensureBLEExists() {
		if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
			return false;
		}
		//獲取BluetoothAdapter
		BluetoothManager bm = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
		if (bm!=null) mBluetoothAdapter = bm.getAdapter();
		return true;
	}
第三步 註冊廣播,開啓藍牙
 /**
	* 註冊藍牙狀態改變的監聽廣播
	*/
	private void registerBlueToothReceiver(){
	    if (mBluetoothStateReceiver==null)mBluetoothStateReceiver=new BluetoothEnableStateReceiver();
	    IntentFilter filter = new IntentFilter();
	    filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
	    registerReceiver(mBluetoothStateReceiver,filter);
	}

	//藍牙開關狀態的廣播接收者,可以通過設置接口回調進行監聽,
	//以方便在藍牙狀態變化的時候做出相應操作或提示
	public class BluetoothEnableStateReceiver extends BroadcastReceiver {
	    @Override
	    public void onReceive(Context context, Intent intent) {
	        String action = intent.getAction();
	        if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
	            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
	            Log.i(TAG, "BluetoothOnOffStateReceiver: state: " + state);
	            if(state == BluetoothAdapter.STATE_ON) {
	               //藍牙打開
	                
	            } else if(state == BluetoothAdapter.STATE_TURNING_OFF){
	                 //藍牙正在關閉
	                 
	            } else if(state == BluetoothAdapter.STATE_OFF){
	             	 //藍牙已關閉
	            }
	        }
	    }
	}


  /**
     * 開啓藍牙
     */
    public void enableBluetooth() {
        if (mBluetoothAdapter!=null){
            if (!mBluetoothAdapter.isEnabled()) { //藍牙未開啓,通過隱式意圖請求開啓藍牙
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, 0);
            }
        }
    }
第四步 掃描指定類型的設備

用戶允許開啓藍牙之後接下來就可以掃描周邊的設備了

public BluetoothAdapter.LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { //device是設備對象,rssi是信號強度,scanRecord是掃描記錄
	        if (device != null) {
	          //接口回調掃描到的設備
	            synchronized (mCallBacks){
	                for (BleAdapterCallBack callBack : mCallBacks) {
	                    callBack.onDeviceFound(device, rssi);
	                }
	            }
        }
    };

	/**
     * 開始掃描 10秒後自動停止
     * */
    private void startScan(){
	    UUID[] uuid = {UUID_SERVICE };
	  	if(mIsScanning){ //如果當前正在掃描則先停止掃描
	  		mBluetoothAdapter.stopLeScan(mScanCallback);
	  	}
		//mBluetoothAdapter.startLeScan(scanCallback);//不進行特定設備過濾,掃描所有設備
		//進行特定uuid過濾,只掃描具有指定Service UUID的設備
		mBluetoothAdapter.startLeScan(uuid, mScanCallback);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //結束掃描
                mBluetoothAdapter.stopLeScan(scanCallback);
           	 }
        },10000);
    }

    藍牙掃描是比較耗費資源的,如果掃描頻率比較高或者時間比較長,在性能差一點手機上會出現電量消耗比較大和發熱比較嚴重的情況,所以要設置適當的掃描時間。
    另外,我們可以調用mBluetoothAdapter.startLeScan(uuid, mScanCallback),掃描具有指定Service UUID的設備,也可以調用mBluetoothAdapter.startLeScan(scanCallback),掃描所有的藍牙設備,可以根據不同的方法自行選擇。

第五步 連接設備,獲取特徵值
BluetoothGattCallback  gattCallback = new BluetoothGattCallback() {

	//連接狀態變化的回調
	@Override
	public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
		super.onConnectionStateChange(gatt, status, newState);
		Log.i(TAG, "連接狀態:status:" + status + ",newState:" + newState)
		 if (status == BluetoothGatt.GATT_SUCCESS) {
			if (newState == BluetoothProfile.STATE_CONNECTED) {
				//連接成功,調用發現服務的方法
				gatt.discoverServices();
				mHandler.sendEmptyMessage(STATE_CONNECTED);
			} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
				Log.i(TAG, "斷開連接");
				mHandler.sendEmptyMessage(STATE_DISCONNECT);
				gatt.disconnect();
				gatt.close();
			}
		} else {
			Log.i(TAG, "連接失敗");
			gatt.close();
		}
	}
	
	//發現Service的回調
	@Override
	public void onServicesDiscovered(BluetoothGatt gatt, int status) {
		super.onServicesDiscovered(gatt, status);
		if (status == BluetoothGatt.GATT_SUCCESS) {
			//if(D) Log.i(TAG, "onServicesDiscovered success.");
			mBluetoothGatt = gatt;
			BluetoothGattService service = gatt.getService(UUID_SERVICE);
			if (service == null) {
				close();
				return;
			}
			//獲取對應的特徵值
			mReadCharacteristic = service.getCharacteristic(UUID_READ);
			mWriteCharacteristic = service.getCharacteristic(UUID_WRITE);
			mNotifyCharacteristic = service.getCharacteristic(UUID_NOTIFY);
			//開啓mNotifyCharacteristic特徵的通知
			gatt.setCharacteristicNotification(mNotifyCharacteristic, true);
		}
	}
	
	//讀操作回調
	@Override
	public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
		super.onCharacteristicRead(gatt, characteristic, status);
		
	}

	//寫操作的回調
	@Override
	public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
		super.onCharacteristicWrite(gatt, characteristic, status);
		//                            Log.i(TAG,"onCharacteristicWrite:"+status);
		if (characteristic.getUuid().equals(UUID_WRITE)) {
			if (status == BluetoothGatt.GATT_SUCCESS) {
				synchronized (mCallBacks){
					for (BleAdapterCallBack callBack : mCallBacks) {
						callBack.onDataSendOk(true);
					}
				}
			} else {
				synchronized (mCallBacks){
					for (BleAdapterCallBack callBack : mCallBacks) {
						callBack.onDataSendOk(false);
					}
				}
			}
		}
	}

	//接收到連接設備傳送過來的數據的回調
	@Override
	public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
		super.onCharacteristicChanged(gatt, characteristic);
		byte[] data = characteristic.getValue();	//取出接收到的數據
		if (characteristic.getUuid().equals(UUID_NOTIFY)) {
			Message message = Message.obtain();
			message.obj=data;
			message.what=RECEIVE_DATA;
			mHandler.sendMessage(message);
		}
	}
}

//連接設備
public void connect(BluetoothDevice  device)
	if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) {
			mBluetoothGatt = device.connectGatt(MainActivity.this,false, gattCallback, BluetoothDevice.TRANSPORT_LE);
	} else {
		mBluetoothGatt = device.connectGatt(MainActivity.this,false, gattCallback);
	}
}	

/**
* 發送數據
* @param data
*/
protected void writeData(byte[] data) {
	  if (mWriteCharacteristic!=null){
		    mWriteCharacteristic.setValue(data);
		    mBluetoothGatt.writeCharacteristic(mWriteCharacteristic);
	 }
}

/**
* 從遠程設備讀取請求的特徵(比較少用)
* @param data
*/
private boolean readData() {
       return mBluetoothGatt.readCharacteristic(mReadCharacteristic);
    }

在這裏有幾個需要注意的問題:
首先,在連接成功之後記得調用gatt.discoverServices(),否則不會回調onServicesDiscovered(),則無法建立連接;其次,在進行連接時系統提供了兩個不同的方法:

public BluetoothGatt connectGatt(Context context, boolean autoConnect,
            BluetoothGattCallback callback) {
        return (connectGatt(context, autoConnect, callback, TRANSPORT_AUTO));
    }
    
public BluetoothGatt connectGatt(Context context, boolean autoConnect,
            BluetoothGattCallback callback, int transport) {
        return (connectGatt(context, autoConnect, callback, transport, PHY_LE_1M_MASK));
    }

connectGatt(Context context, boolean autoConnect,BluetoothGattCallback callback, int transport)這個方法在6.0之前的隱藏的,6.0纔開始開放,兩個方法只差一個transport參數,這個參數主要用來設置連接模式,對於這個參數系統提供了三個常量:

BluetoothDevice.TRANSPORT_AUTO:對於GATT連接到遠程雙模設備無物理傳輸優先。
BluetoothDevice.TRANSPORT_BREDR:GATT連接到遠程雙模設備優先BR/EDR。
BluetoothDevice.TRANSPORT_LE:GATT連接到遠程雙模設備優先BLE。

我們這是Ble設備,所以我們選擇BluetoothDevice.TRANSPORT_LE模式。

容易碰到的一些錯誤和問題
  • 連接不成功,Log提示“BluetoothGatt status 133”

問題:連接總是不成功,onConnectionStateChange()方法中的status 爲133。

    出現這個問題的主要的主要原因之一是在連接失敗或者連接斷開後沒有調用gatt.close()進行資源的釋放,所以要注意gatt資源的及時釋放;另外在6.0及以上的設備連接時記得使用可以設置連接模式的方法,可以大概率降低這個133問題出現的概率。

  • 單條數據大小超出20個byte

    雖然在制定協議的時候會在保證安全性的前提下儘量簡潔,但是有時候可能會有那麼幾條通信指令的數據量超出20個byte,畢竟包序號、校驗值等都要佔用幾個字節,這種時候這麼辦呢?
    碰到這種情況我們就必須對數據進行分包傳送接收:可以和制定協議的人約定每條數據的開頭標誌、分包數量、結束標誌等,這樣就可以很清楚的知道當前是不是第一條數據以及後面還有沒有數據等。

總結

我們再來回顧一下主要流程:

    獲取BluetoothAdapter->開啓藍牙->掃描指定類型設備->設備連接->獲取gatt和相關Characteristic,基本的流程就是這些,大家可以根據自己的需求將整個流程封裝到一個工具類中,方便使用。

文章末尾給大家推薦一個不錯的藍牙調試工具nRF Connect,該工具可以很方便的查看到連接設備的詳細信息,包括Service信息、各個Characteristic的信息和屬性等等。

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