概述
中心角色和外圍角色
在BLE中存在兩個角色,一個是中心角色(Central),一個是外圍角色(Peripheral),藍牙設備或手機都可以單獨作為Central或Peripheral角色。外設角色的作用是為中心角色提供各種數據,中心角色可以掃描并接收多個外設角色數據( 外圍角色中的設備進行廣播,中心角色的設備掃描尋找廣播),數據以服務(Service)和特征(Characteristic)的形式呈現。其中Ble中心角色的API在Android 4.3得到支持,而外圍角色的API在Android 5.0才得到支持
協議、服務、特征、描述符
一份協議(BluetoothGatt)由一個或多個服務(BluetoothGattService)構成,一個服務由零個或多個特征(BluetoothGattCharacteristic)構成,一個特征可以包含零個或多個描述符(BluetoothGattDescriptor)。每一個服務、特征、描述符都有一個UUID作為唯一識別符,識別符有通用的,也可以自定義,也可以隨機生成,固定格式00000000-0000-0000-0000-000000000000(8-4-4-4-12),一般來說自定義的UUID只有前8位有變化,后面的基本是固定的0000-1000-8000-00805f9b34fb,所以一個自定義的UUID一般看起來就像這樣 “0000????-0000-1000-8000-00805f9b34fb” 通配符就表示4個16進制數。每一個特征都有其屬性和權限(Read | Write | Notify | Indicate),特征根據屬性可讀可寫。在每個Ble藍牙設備中,都會有兩個默認的服務如下:
//Generic Access(Generic Attribute Profile 通用屬性規范GATT)service:00001801-0000-1000-8000-00805f9b34fbcharacteristic:00002a05-0000-1000-8000-00805f9b34fb//Generic Attribute (Generic Access Profile 通用接入規范GAP)service:00001800-0000-1000-8000-00805f9b34fbcharacteristic:00002a00-0000-1000-8000-00805f9b34fbcharacteristic:00002a01-0000-1000-8000-00805f9b34fbcharacteristic:00002aa6-0000-1000-8000-00805f9b34fb
適配器,掃描器:
每一臺支持藍牙的手機中都會有一個藍牙適配器,由藍牙管理器管理著,從其中獲得藍牙適配器。適配器中自帶掃描器,使用掃描器可以掃描周邊的藍牙設備。
Ble中常用類
BluetoothDeivce:藍牙設備,代表一個具體的藍牙外設
BluetoothManager:藍牙管理器,主要用于獲取藍牙適配器和管理所有和藍牙相關的東西
BluetoothAdapter:藍牙適配器,每一臺支持藍牙功能的手機都有一個藍牙適配器,一般來說,只有一個,可以通過BluetoothManager獲取
BluetoothLeScanner:藍牙適配器里面的掃描器,用于掃描BLE外設,可以通過BluetoothAdapter獲取
BluetoothGatt:通用屬性協議,定義了BLE通訊的基本規則,就是通過把數據包裝成服務和特征的約定過程,可以通過建立連接獲取
BluetoothGattService:服務,描述了一個BLE設備的一項基本功能,由零或多個特征組構成,可以通過BluetoothGatt獲取并自定義(現有的約定服務可以在bluetooth.org上找到)
BluetoothGattCallback:作為中央的回調類,用于回調GATT通信的各種狀態和結果
BluetoothGattServerCallback:作為周邊的回調類,用于回調GATT通信的各種狀態和結果
BluetoothGattCharacteristic:特征,里面包含了一組或多組數據,是GATT通信中的最小數據單元。
BluetoothGattDescriptor:特征描述符,對特征的額外描述,包括但不僅限于特征的單位,屬性等。
BluetoothProfile:一個通用的規范,按照這個規范來收發數據
使用流程
中心設備:判斷藍牙是否可用->打開藍牙->開始掃描->獲取被掃描到的設備->連接設備->發現服務->獲取到指定特征->寫入特征值
外圍設備:判斷藍牙是否可用->打開藍牙->創建廣播數據->發送廣播->添加服務至廣播->根據監聽獲取寫入的數據
下圖是中心設備的使用流程圖來源
image.png
權限聲明
<uses-permission android:name="android.permission.BLUETOOTH"/><uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>//獲取掃描結果權限,Android6.0以上需要動態權限(這兩個權限是在經典藍牙的掃描中需要聲明的)<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>//或者<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>//當android:required為true的時候,app只能強制運行在支持BLE功能的設備商,為false的時候,可以運行在所有設備上,但某些方法需要手動檢測,否則可能存在隱性BUG<uses-feature android:name="android.hardware.bluetooth_le"android:required="false"/>
Ble可用性判斷
不是任何設備都支持BLE,最開始要確定設備是否支持,還要確定藍牙已經打開。
Android系統從4.3以后才支持,這是先決條件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2){? ? ... ...}
判斷藍牙是否可用
//小于等于API17時直接使用BluetoothAdapter.getDefaultAdapter()來獲取AdapterprivateBluetoothAdaptergetAdapter(){if(android.os.Build.VERSION.SDK_INT>=android.os.Build.VERSION_CODES.JELLY_BEAN_MR2){mBluetoothManager=(BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);mBluetoothAdapter=mBluetoothManager.getAdapter();}else{mBluetoothAdapter=BluetoothAdapter.getDefaultAdapter();}returnmBluetoothAdapter;}if(mBluetoothAdapter==null){//藍牙不可用}//Ble不可用privatebooleancheckIfSupportBle(){returngetPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);}
藍牙開啟
//開啟藍牙功能需要一小段時間,所以不能執行開啟藍牙立即執行其他操作,這時藍牙實際還沒有開啟,回出現異常,所以后續操作應該在藍牙狀態廣播中處理privatevoidenableBluetooth(){if(mBluetoothAdapter==null&&!mBluetoothAdapter.isEnabled()){//使用系統彈框來啟動藍牙,REQUEST_ENABLE_BT為自定義的開啟藍牙請求碼IntentenableBtIntent=newIntent(BluetoothAdapter.ACTION_REQUEST_ENABLE);startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);//或者靜默打開bluetoothAdapter.enable();}}//靜默關閉bluetoothAdapter.disable();
藍牙開關廣播更多藍牙相關廣播
//廣播的ActionBluetoothAdapter.ACTION_STATE_CHANGED//四種狀態值? ? BluetoothAdapter.STATE_ON//已開啟BluetoothAdapter.STATE_OFF//已關閉BluetoothAdapter.STATE_TURNING_ON//正在開啟BluetoothAdapter.STATE_TURNING_OFF//正在關閉//當前狀態值的獲取intcurState=intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,BluetoothAdapter.STATE_ON);
掃描具體使用流程
經典通用方式
//此過程大概持續10秒,當掃描到藍牙設備后,會發出廣播,只要在需要的地方注冊接收廣播,就可以獲得掃描結果。這種方法可以掃描出所有藍牙設備,包括BLE,但貌似不同手機有不同體驗。privatevoidstartDiscover(){mBluetoothAdapter.startDiscover();}//注冊此廣播,監聽BluetoothDevice.ACTION_FOUND,以接收系統消息取得掃描結果 privateclassDeviceReceiverextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(BluetoothDevice.ACTION_FOUND.equals(action)){//這個就是所獲得的藍牙設備。BluetoothDevicedevice=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);mDevices.add(device);}}}
Ble掃描方式
//Ble藍牙掃描方式在5.0之后發生了變更if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP){BluetoothLeScannerbluetoothLeScanner=bluetoothAdapter.getBluetoothLeScanner();//過濾器設置ArrayList<ScanFilter>filters=newArrayList<>();ScanFilter.BuilderfilterBuilder=newScanFilter.Builder();filters.add(filterBuilder);//掃描結果回調設置ScanCallbackscanCallback=newScanCallback(){publicvoidonScanResult(intcallbackType,ScanResultresult){//scanResult.getDevice()取得device}};//掃描參數設置ScanSettings.BuildersettingsBuilder=newScanSettings.Builder();//這種掃描方式占用資源比較高,建議當應用處于前臺時使用該模式settingsBuilder.setScanMode(SCAN_MODE_LOW_LATENCY);// 開啟掃描,第三項參數見下面,該操作為異步操作的,但是會消耗大量資源,一般掃描時長為12秒,建議找到需要的設備后,執行取消掃描bluetoothLeScanner.startScan(scanCallback,filter,settingsBuilder.build());}else{//掃描結果回調設置? BluetoothAdapter.LeScanCallbackleScanCallback=newBluetoothAdapter.LeScanCallback(){publicvoidonLeScan(BluetoothDevicedevice,intrssi,byte[]scanRecord){}};//該方法已被聲明為過時方法 adapter.startLeScan(leScanCallback)}
停止掃描
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){? bluetoothLeScanner.stopScan(mScanCallback);}else{? ? bluetoothAdapter.stopLeScan(mLeScanCallback);}
設備信息獲取
根據藍牙地址獲取藍牙設備
BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(address);
已綁定設備獲取
privatevoidgetBoundDevices(){Set<BluetoothDevice>boundDevices=mBluetoothAdapter.getBondedDevices();for(BluetoothDevice device:boundDevices){//對device進行其他操作,比如連接等。}}
設備詳細信息獲取
privatevoidshowDetailOfDevice(){//獲得設備名稱,多個設備可以有同一個名稱。String deviceName=bluetoothDevice.getName();//獲取設備物理地址,一個設備只能有一個物理地址,每個設備都有每個設備的物理地址,無法相同。String deviceMacAddress=bluetoothDevice.getAddress();//綁定設備bluetoothDevice.createBond();}
連接
建立連接
//掃描回調的BluetoothDevice用于建立GATT連接,參數2是控制是否自動鏈接,為true的時候,當設備進入中心范圍,會進行自動連接,為false時是主動連接,一般推薦使用主動連接,因為這樣連接速度更快。BluetoothGatt bluetoothGatt = bluetoothDevice.connectGatt(context, false, bluetoothGattCallback);//如果已經連接后,但是又斷開了連接,需要重新連接時bluetoothGatt.connect();
連接狀態的獲取
//連接狀態改變會在BluetoothGattCallback的onConnectionStateChange方法中回調,同時也可以隨時主動去查詢intstate=bluetoothManager.getConnectionState(bluetoothDevice,BluetoothGatt.GATT);
發現服務
//在建立連接成功后進行發現服務,onServicesDiscovered為發現服務的回調bluetoothGatt.discoverServer();
特征改變
//該方法一般是在發現服務后,進行設置的,設置該方法的目的是讓硬件在特征數據改變的時候,發送數據給app,app則通過onCharacteristicChanged方法回調給用戶,從參數中可獲取到回調回來的數據bluetoothGatt.setCharacteristicNotification(characteristic,true);
連接數據回調(在非UI線程)
//mBluetoothGattCallback 為所有藍牙數據回調的處理者,也是整個藍牙操作當中最為核心的一部分。它里面有很多方法,但并非所有都需要在開發當中用到,這里列出來只是作為部分解析,需要哪個方法,就重寫哪個方法,不需要的,直接去掉privateBluetoothGattCallbackmBluetoothGattCallback=newBluetoothGattCallback(){@OverridepublicvoidonConnectionStateChange(BluetoothGattgatt,intstatus,intnewState){super.onConnectionStateChange(gatt,status,newState);//當設備與中心連接狀態發生改變時,下面是已連接的判斷if(status==BluetoothGatt.GATT_SUCCESS&&newState==BluetoothProfile.STATE_CONNECTED){...}}@OverridepublicvoidonServicesDiscovered(BluetoothGattgatt,intstatus){super.onServicesDiscovered(gatt,status);//當發現設備服務時,會回調到此處,下面是遍歷所有發現的服務if(status==BluetoothGatt.GATT_SUCCESS){//gatt.getServices()可以獲得外設的所有服務for(BluetoothGattServiceservice:gatt.getServices()){//每發現一個服務,我們再次遍歷服務當中所包含的特征,service.getCharacteristics()可以獲得當前服務所包含的所有特征for(BluetoothGattCharacteristiccharacteristic:service.getCharacteristics()){//通常可以把所發現的特征放進一個列表當中以便后續操作,如果你想知道每個特征都包含哪些描述符,很簡單,再用一個循環去遍歷每一個特征的getDescriptor()方法。mCharacteristics.add(characteristic);//UUID獲取,或者根據UUID判斷是否是自己需要的服務/特征Log.i("",characteristic.getUuid().toString());//打印特征的UUID。}}}}@OverridepublicvoidonCharacteristicRead(BluetoothGattgatt,BluetoothGattCharacteristiccharacteristic,intstatus){//讀取特征后回調到此處。}@OverridepublicvoidonCharacteristicWrite(BluetoothGattgatt,BluetoothGattCharacteristiccharacteristic,intstatus){//寫入特征后回調到此處,status == BluetoothGatt.GATT_SUCCESS代表寫入成功}@OverridepublicvoidonCharacteristicChanged(BluetoothGattgatt,BluetoothGattCharacteristiccharacteristic){//當特征(值)發生變法時回調到此處。}@OverridepublicvoidonDescriptorRead(BluetoothGattgatt,BluetoothGattDescriptordescriptor,intstatus){//讀取描述符后回調到此處。}@OverridepublicvoidonDescriptorWrite(BluetoothGattgatt,BluetoothGattDescriptordescriptor,intstatus){//寫入描述符后回調到此處}@OverridepublicvoidonReliableWriteCompleted(BluetoothGattgatt,intstatus){//暫時沒有用過。}@OverridepublicvoidonReadRemoteRssi(BluetoothGattgatt,intrssi,intstatus){//Rssi表示設備與中心的信號強度,發生變化時回調到此處。}@OverridepublicvoidonMtuChanged(BluetoothGattgatt,intmtu,intstatus){//暫時沒有用過。}};
數據傳輸
寫入特征值,會回調onCharacteristicWrite
BluetoothGattService service=bluetoothGatt.getService(SERVICE_UUID);BluetoothGattCharacteristic characteristic=service.getCharacteristic(CHARACTERISTIC_UUID);characteristic.setValue(writeString);//設置回復方式bluetoothGattCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);bluetoothGatt.writeCharacteristic(characteristic);
寫入描述符
bluetoothGattDescriptor.setValue(writeContent.getBytes());bluetoothGatt.writeDescriptor(bluetoothGattDescriptor);
讀取特征值
bluetoothGatt.readCharacteristic(characteristic);
外圍設備實現(被連接者/服務端 )
廣播的設定
privateAdvertiseSettingsbuildAdvertiseSettings(){AdvertiseSettings.Builder builder=newAdvertiseSettings.Builder()//設置廣播模式:低功耗、平衡、低延時,廣播間隔時間依次越來越短.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)//設置是否可以連接,一般不可連接廣播應用在iBeacon設備上.setConnectable(true)//設置廣播的最長時間,最大時長為LIMITED_ADVERTISING_MAX_MILLIS(180秒).setTimeout(10*1000)//設置廣播的信號強度 ADVERTISE_TX_POWER_ULTRA_LOW, ADVERTISE_TX_POWER_LOW,//ADVERTISE_TX_POWER_MEDIUM, ADVERTISE_TX_POWER_HIGH 信號強度依次增強.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH);returnbuilder.build();}
對外廣播的數據(數據限制 31 Bytes)
privateAdvertiseDatabuildAdvertiseData(){AdvertiseData.BuilderdataBuilder=newAdvertiseData.Builder();//添加廠家信息,第一個參數為廠家ID(不足兩個字節會自動補0,例如這里為0x34,實際數據則為34,00)//一般情況下無需設置,否則會出現無法被其他設備掃描到的情況.addManufacturerData(0x34,newbyte[]{0x56})//添加服務進行廣播,即對外廣播本設備擁有的服務.addServiceData(...);//是否廣播信號強度.setIncludeTxPowerLevel(true)//是否廣播設備名稱.setIncludeDeviceName(true);returndataBuilder.build();}
對外廣播(即允許被掃描到)
privatevoidadvertise(){BluetoothManagerbluetoothManager=(BluetoothManager)getSystemService(BLUETOOTH_SERVICE);BluetoothAdaptermAdapter=bluetoothManager.getAdapter();AdvertiseCallbackadvertiseCallback=newAdvertiseCallback(){@OverridepublicvoidonStartSuccess(AdvertiseSettingssettingsInEffect){//廣播成功,建議在這里開啟服務}@OverridepublicvoidonStartFailure(interrorCode){//廣播失敗}};BluetoothLeAdvertisermAdvertiser=mAdapter.getBluetoothLeAdvertiser();mAdvertiser.startAdvertising(buildAdvertiseSettings(),buildAdvertiseData(),mAdvertiseCallback);}
開啟服務
//聲明需要廣播的服務的UUID和特征的UUID,注意不要占用藍牙設備默認的UUIDUUID UUID_SERVICE=UUID.fromString("00001354-0000-1000-8000-00805f9b34fb");UUID UUID_CHARACTERISTIC=UUID.fromString("00001355-0000-1000-8000-00805f9b34fb");UUID UUID_DESCRIPTOR=UUID.fromString("00001356-0000-1000-8000-00805f9b34fb");//外圍設備狀態、數據回調,詳情見后面BluetoothGattServerCallback serverCallback=newBluetoothGattServerCallback(){...};//GATT協議服務BluetoothGattServer server=bluetoothManager.openGattServer(this,serverCallback);//創建一個服務BluetoothGattService service=newBluetoothGattService(UUID_SERVICE,BluetoothGattService.SERVICE_TYPE_PRIMARY);//創建一個特征,首先此characteristic屬性滿足BluetoothGattCharacteristic.PROPERTY_WRITY或BluetoothGattCharacteristic.PROPERTY_WRITY_NO_RESPONSE,//如果其property都不包含這兩個,寫特征值writeCharacteristic()函數直接返回false,什么都不做處理。其次此characteristic權限應滿足//BluetoothGattCharacteristic.PERMISSION_WRITE,否則onCharacteristicWrite()回調收到GATT_WRITE_NOT_PERMITTED回應//如果需要既能讀,又能寫,則可以參考如下寫法BluetoothGattCharacteristic characteristic=newBluetoothGattCharacteristic(Constants.UUID_CHARACTERISTIC,BluetoothGattCharacteristic.PROPERTY_READ|BluetoothGattCharacteristic.PROPERTY_WRITE,BluetoothGattCharacteristic.PERMISSION_WRITE|BluetoothGattCharacteristic.PERMISSION_READ);//創建一個描述符BluetoothGattDescriptor descriptor=newBluetoothGattDescriptor(UUID_DESCRIPTOR,BluetoothGattDescriptor.PERMISSION_READ);//將描述符添加到特征中,一個特征可以包含0至多個描述符characteristic.addDescriptor(descriptor);//將特征添加到服務中,一個服務可以包含1到多個特征service.addCharacteristic(characteristic);server.addService(service);
BluetoothGattServerCallback
publicabstractclassBluetoothGattServerCallback{publicBluetoothGattServerCallback(){}publicvoidonConnectionStateChange(BluetoothDevicedevice,intstatus,intnewState){//連接狀態被改變}publicvoidonServiceAdded(intstatus,BluetoothGattServiceservice){//添加服務}publicvoidonCharacteristicReadRequest(BluetoothDevicedevice,intrequestId,intoffset,BluetoothGattCharacteristiccharacteristic){//被讀取特征}publicvoidonCharacteristicWriteRequest(BluetoothDevicedevice,intrequestId,BluetoothGattCharacteristiccharacteristic,booleanpreparedWrite,booleanresponseNeeded,intoffset,byte[]value){//被寫入數據,其中device是寫入的設備,value是寫入的值,responseNeeded指是否需要恢復,如果需要恢復則調用gattServer.sendResponse()方法回復}publicvoidonDescriptorReadRequest(BluetoothDevicedevice,intrequestId,intoffset,BluetoothGattDescriptordescriptor){//被讀取描述符}publicvoidonDescriptorWriteRequest(BluetoothDevicedevice,intrequestId,BluetoothGattDescriptordescriptor,booleanpreparedWrite,booleanresponseNeeded,intoffset,byte[]value){//被寫入描述符}publicvoidonExecuteWrite(BluetoothDevicedevice,intrequestId,booleanexecute){}@OverridepublicvoidonNotificationSent(BluetoothDevicedevice,intstatus){}@OverridepublicvoidonMtuChanged(BluetoothDevicedevice,intmtu){}@OverridepublicvoidonPhyUpdate(BluetoothDevicedevice,inttxPhy,intrxPhy,intstatus){}@OverridepublicvoidonPhyRead(BluetoothDevicedevice,inttxPhy,intrxPhy,intstatus){}}
廣播數據格式
廣播數據(或者掃描應答數據)由一個一個的AD Structure組成,對于未滿31bytes的其它數據,則填充為0;每個AD Structure由兩部分組成:1byte的長度信息(Data的長度),和剩余的Data信息;
Data信息又由兩部分組成:AD Type(長度不定)指示該AD Structure的類型,以及具體的AD Data。
image.png
例如:
02 01 06 03 03 aa fe 17 16 aa fe 00 -10 00 01 02 03 04 05 06 07 08 09 0a 0b 0e 0f 00 00 00 00
02 01 06是一個AD Structure:Data的長度是02;Data是01 06;AD Type是01(Flags);AD Data是06,表明支持General Discoverable Mode、不支持BR/EDR。
03 03 aa fe是一個AD Structure:Data的長度是03;Data是03 aa fe;AD Type是03(16 bits的Service UUID);AD Data是aa fe,是Eddystone profile的Service UUID。AD Type查詢
注意
外圍設備廣播的數據最多31 byte
寫特征一次最多寫入20 byte
單次掃描時間不宜過長(建議10~15秒)
BluetoothGattCallback的回調是在非UI線程
每個設備所能使用的Gatt連接是有限個數的,所以應該斷開連接時也關閉GattbluetoothGatt.close(),然后重新連接
外圍設備藍牙關閉后,中心設備的BluetoothGattCallback的onConnectionStateChange會回調狀態碼status = 19,newState = 0(斷開連接)
外圍設備藍牙關閉后,中心設備再進行連接,BluetoothGattCallback的onConnectionStateChange會回調狀態碼133,并且當外圍設備藍牙開啟后再調用中心設備的bluetoothGatt.connect()不能重新連接,仍然是133錯誤,所以建議針對133錯誤進行關閉gatt并釋放資源