android BLE系列:
手機作為外設
在藍芽開發中,有些情況是不需要連線的,只要外設廣播自己的資料即可,例如蘋果的ibeacon。自Android 5.0更新藍芽API後,手機可以作為外設廣播資料。
廣播包有兩種:
- 廣播包(Advertising Data)
- 響應包(Scan Response)
其中廣播包是每個外設都必須廣播的,而響應包是可選的。每個廣播包的長度必須是31個位元組,如果不到31個位元組 ,則剩下的全用0填充。 補全。這部分的資料是無效的
廣播資料單元
廣播包中包含若干個廣播資料單元,廣播資料單元也稱為 AD Structure。
廣播資料單元 = 長度值Length + AD type + AD Data。
長度值Length只佔一個位元組,並且位於廣播資料單元的第一個位元組。
概念的東西有些抽象,先看看下面的廣播報文:
0x代表這串字串是十六進位制的字串。兩位十六進位制數代表一個位元組。因為兩個字元組成的十六進位制字串最大為FF,即255,而Java中byte型別的取值範圍是-128到127,剛好可以表示一個255的大小。所以兩個十六進位制的字串表示一個位元組。
繼續檢視報文內容,開始讀取第一個廣播資料單元。讀取第一個位元組:0x07,轉換為十進位制就是7,即表示後面的7個位元組是這個廣播資料單元的資料內容。超過這7個位元組的資料內容後,表示是一個新的廣播資料單元。
而第二個廣播資料單元,第一個位元組的值是0x16,轉換為十進位制就是22,表示後面22個位元組為第二個廣播資料單元。
在廣播資料單元的資料部分中,第一個位元組代表資料型別(AD type),決定資料部分表示的是什麼資料。(即廣播資料單元第二個位元組為AD type)
AD Type的型別如下:
- Flags:TYPE = 0x01。用來標識裝置LE物理連線。
bit 0: LE 有限發現模式
bit 1: LE 普通發現模式
bit 2: 不支援 BR/EDR
bit 3: 對 Same Device Capable(Controller) 同時支援 BLE 和 BR/EDR
bit 4: 對 Same Device Capable(Host) 同時支援 BLE 和 BR/EDR
bit 5..7: 預留
這bit 1~7分別代表著傳送該廣播的藍芽晶片的物理連線狀態。當bit的值為1時,表示支援該功能。 例:
- Service UUID。廣播資料中可以將裝置支援的GATT Service的UUID廣播出來,來告知中心裝置其支援的Service。
對於不同bit的UUID,其對應的型別也有不同:
非完整的16bit UUID: TYPE = 0x02;
完整的16bit UUID 列表: TYPE = 0x03;
非完整的32bit UUID 列表: TYPE = 0x04;
完整的32bit UUID 列表: TYPE = 0x05;
非完整的128bit UUID 列表: TYPE = 0x06;
完整的128bit UUID: TYPE = 0x07;
- TX Power Level: TYPE = 0x0A,表示裝置傳送廣播包的訊號強度。 數值範圍:±127 dBm。
- 裝置名字,DATA 是名字的字串,可以是裝置的全名,也可以是裝置名字的縮寫。
縮寫的裝置名稱: TYPE = 0x08
完整的裝置名稱: TYPE = 0x09
- Service Data: Service 對應的資料。
16 bit UUID Service: TYPE = 0x16, 前 2 位元組是 UUID,後面是 Service 的資料;
32 bit UUID Service: TYPE = 0x20, 前 4 位元組是 UUID,後面是 Service 的資料;
128 bit UUID Service: TYPE = 0x21, 前 16 位元組是 UUID,後面是 Service 的資料;
- 廠商自定義資料: TYPE = 0xFF。 廠商資料中,前兩個位元組表示廠商ID,剩下的是廠商自定義的資料。
BLE廣播
藍芽廣播的資料格式大致講了一下,有助於下面的廣播操作的理解。
先自定義UUID:
//UUID
public static UUID UUID_SERVICE = UUID.fromString("0000fff7-0000-1000-8000-00805f9b34fb");
複製程式碼
開啟廣播一般需要3~4物件:廣播設定(AdvertiseSettings)、廣播包(AdvertiseData)、掃描包(可選)、廣播回撥(AdvertiseCallback)。
先看看廣播設定(AdvertiseSettings)如何定義:
//初始化廣播設定
mAdvertiseSettings = new AdvertiseSettings.Builder()
//設定廣播模式,以控制廣播的功率和延遲。
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER)
//發射功率級別
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
//不得超過180000毫秒。值為0將禁用時間限制。
.setTimeout(3000)
//設定是否可以連線
.setConnectable(false)
.build();
複製程式碼
(1)、通過AdvertiseSettings.Builder#setAdvertiseMode() 設定廣播模式。其中有3種模式:
1、在均衡電源模式下執行藍芽LE廣播:AdvertiseSettings#ADVERTISE_MODE_BALANCED
2、在低延遲,高功率模式下執行藍芽LE廣播: AdvertiseSettings#ADVERTISE_MODE_LOW_LATENCY
3、在低功耗模式下執行藍芽LE廣播:AdvertiseSettings#ADVERTISE_MODE_LOW_POWER
(2)、通過AdvertiseSettings.Builder#setAdvertiseMode() 設定廣播發射功率。共有4種功率模式:
1、使用高TX功率級別進行廣播:AdvertiseSettings#ADVERTISE_TX_POWER_HIGH
2、使用低TX功率級別進行廣播:AdvertiseSettings#ADVERTISE_TX_POWER_LOW
3、使用中等TX功率級別進行廣播:AdvertiseSettings#ADVERTISE_TX_POWER_MEDIUM
4、使用最低傳輸(TX)功率級別進行廣播:AdvertiseSettings#ADVERTISE_TX_POWER_ULTRA_LOW
(3)、通過AdvertiseSettings.Builder#setTimeout()設定持續廣播的時間,單位為毫秒。最多180000毫秒。當值為0則無時間限制,持續廣播,除非呼叫BluetoothLeAdvertiser#stopAdvertising()。
(4)、通過AdvertiseSettings.Builder#setConnectable()設定該廣播是否可以連線的。
之前說過,外設必須廣播廣播包,掃描包是可選。但新增掃描包也意味著廣播更多得資料,即可廣播62個位元組。
//初始化廣播包
mAdvertiseData = new AdvertiseData.Builder()
//設定廣播裝置名稱
.setIncludeDeviceName(true)
//設定發射功率級別
.setIncludeDeviceName(true)
.build();
//初始化掃描響應包
mScanResponseData = new AdvertiseData.Builder()
//隱藏廣播裝置名稱
.setIncludeDeviceName(false)
//隱藏發射功率級別
.setIncludeDeviceName(false)
//設定廣播的服務UUID
.addServiceUuid(new ParcelUuid(UUID_SERVICE))
//設定廠商資料
.addManufacturerData(0x11,hexStrToByte(mData))
.build();
複製程式碼
可見無論是廣播包還是掃描包,其廣播的內容都是用AdvertiseData類封裝的。
(1)、AdvertiseData.Builder#setIncludeDeviceName()方法,可以設定廣播包中是否包含藍芽的名稱。
(2)、AdvertiseData.Builder#setIncludeDeviceName()方法,可以設定廣播包中是否包含藍芽的發射功率。
(3)、AdvertiseData.Builder#addServiceUuid(ParcelUuid)方法,可以設定特定的UUID在廣播包中。
(4)、AdvertiseData.Builder#addServiceData(ParcelUuid,byte[])方法,可以設定特定的UUID和其資料在廣播包中。
(5)、AdvertiseData.Builder#addManufacturerData(int,byte[])方法,可以設定特定廠商Id和其資料在廣播包中。
從AdvertiseData.Builder的設定中可以看出,如果一個外設需要在不連線的情況下對外廣播資料,其資料可以儲存在UUID對應的資料中,也可以儲存在廠商資料中。但由於廠商ID是需要由Bluetooth SIG進行分配的,廠商間一般都將資料設定在廠商資料。
另外可以通過BluetoothAdapter#setName()設定廣播的名稱
//獲取藍芽設配器
BluetoothManager bluetoothManager = (BluetoothManager)
getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
//設定裝置藍芽名稱
mBluetoothAdapter.setName("daqi");
複製程式碼
先看一個例子,我們分別在廣播包和掃描包中設定AdvertiseData.Builder的每一種廣播報文引數,得到一下報文內容:
(1)、Type = 0x01 表示裝置LE物理連線。
(2)、Type = 0x09 表示裝置的全名
(3)、Type = 0x03 表示完整的16bit UUId。其值為0xFFF7。
(4)、Type = 0xFF 表示廠商資料。前兩個位元組表示廠商ID,即廠商ID為0x11。後面的為廠商資料,具體由使用者自行定義。
(5)、Type = 0x16 表示16 bit UUID的資料,所以前兩個位元組為UUID,即UUID為0xF117,後續為UUID對應的資料,具體由使用者自行定義。
最後繼承AdvertiseCallback自定義廣播回撥。
private class daqiAdvertiseCallback extends AdvertiseCallback {
//開啟廣播成功回撥
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect){
super.onStartSuccess(settingsInEffect);
Log.d("daqi","開啟服務成功");
}
//無法啟動廣播回撥。
@Override
public void onStartFailure(int errorCode) {
super.onStartFailure(errorCode);
Log.d("daqi","開啟服務失敗,失敗碼 = " + errorCode);
}
}
複製程式碼
初始化完畢上面的物件後,就可以進行廣播:
//如果晶片組支援多廣播,則返回true
boolean result1 = mBluetoothAdapter.isMultipleAdvertisementSupported();
if (result1){
//獲取BLE廣播的操作物件。
//如果藍芽關閉或此裝置不支援藍芽LE廣播,則返回null。
mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
if (mBluetoothLeAdvertiser != null){
//開啟廣播
mBluetoothLeAdvertiser.startAdvertising(mAdvertiseSettings,
mAdvertiseData, mScanResponseData, mAdvertiseCallback);
}else {
Log.d("daqi","手機藍芽未開啟");
}
}else {
Log.d("daqi","該手機晶片不支援廣播");
}
複製程式碼
廣播主要是通過BluetoothLeAdvertiser#startAdvertising()方法實現,但在之前需要先獲取BluetoothLeAdvertiser物件。
BluetoothLeAdvertiser物件存在兩個情況獲取為Null:
1、手機藍芽模組不支援BLE廣播
2、藍芽未開啟
所以在呼叫BluetoothAdapter#getBluetoothLeAdvertiser()前,需要先呼叫BluetoothAdapter#isMultipleAdvertisementSupported()檢查手機是否支援傳送BLE廣播。
與廣播成對出現就是BluetoothLeAdvertiser.stopAdvertising()停止廣播了,傳入開啟廣播時傳遞的廣播回撥物件,即可關閉廣播:
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback)
複製程式碼
啟動GATT Service 和 Characteristic
雖然通過廣播告知外邊自身擁有這些Service,但手機自身並沒有初始化Gattd的Service。導致外部的中心裝置連線手機後,並不能找到對應的GATT Service 和 獲取對應的資料。
BluetoothGattService service = new BluetoothGattService(UUID_SERVICE,
BluetoothGattService.SERVICE_TYPE_PRIMARY);
複製程式碼
建立BluetoothGattService時,傳入兩個引數:UUID和Service型別。
Service型別有兩個級別:
- BluetoothGattService#SERVICE_TYPE_PRIMARY 主服務
- BluetoothGattService#SERVICE_TYPE_PRIMARY 次要服務(存在於主服務中的服務)
我們都知道Gatt中,Service的下一級是Characteristic,Characteristic是最小的通訊單元,通過對Characteristic進行讀寫操作來進行通訊。
//初始化特徵值
mGattCharacteristic = new BluetoothGattCharacteristic(UUID_CHARACTERISTIC,
BluetoothGattCharacteristic.PROPERTY_WRITE|
BluetoothGattCharacteristic.PROPERTY_NOTIFY|
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_WRITE|
BluetoothGattCharacteristic.PERMISSION_READ);
複製程式碼
建立BluetoothGattCharacteristic時,傳入三兩個引數:UUID、特徵屬性 和 許可權屬性。
特徵屬性表示該BluetoothGattCharacteristic擁有什麼功能,即能對BluetoothGattCharacteristic進行什麼操作。其中主要有3種:
- BluetoothGattCharacteristic#PROPERTY_WRITE 表示特徵支援通知
- BluetoothGattCharacteristic#PROPERTY_READ 表示特徵支援讀
- BluetoothGattCharacteristic#PROPERTY_NOTIFY 表示特徵支援寫
許可權屬性用於配置該特徵值所具有的功能。主要兩種:
- BluetoothGattCharacteristic#PERMISSION_WRITE 特徵寫許可權
- BluetoothGattCharacteristic#PERMISSION_READ 特徵讀許可權
當特徵值只有讀許可權時,呼叫BluetoothGatt#writeCharacteristic()對特徵值進行修改時,將返回false,無法寫入。並不會觸發BluetoothGattCallback#onCharacteristicWrite()回撥。
當特徵值只有寫許可權時,呼叫BluetoothGatt#readCharacteristic()對特徵值進行讀取時,將返回false,無法寫入。並不會觸發BluetoothGattCallback#onCharacteristicRead()回撥。
Characteristic下還有Descriptor,初始化BluetoothGattDescriptor時傳入:Descriptor UUID 和 許可權屬性
//初始化描述
mGattDescriptor = new BluetoothGattDescriptor(UUID_DESCRIPTOR,BluetoothGattCharacteristic.PERMISSION_WRITE);
複製程式碼
為Service添addCharacteristic,為Characteristic新增Service:
//Service新增特徵值
mGattService.addCharacteristic(mGattCharacteristic);
mGattService.addCharacteristic(mGattReadCharacteristic);
//特徵值新增描述
mGattCharacteristic.addDescriptor(mGattDescriptor);
複製程式碼
通過藍芽管理器mBluetoothManager獲取Gatt Server,用來新增Gatt Service。新增完Gatt Service後,外部中心裝置連線手機時,將能獲取到對應的GATT Service 和 獲取對應的資料
//初始化GattServer回撥
mBluetoothGattServerCallback = new daqiBluetoothGattServerCallback();
if (mBluetoothManager != null)
mBluetoothGattServer = mBluetoothManager.openGattServer(this, mBluetoothGattServerCallback);
boolean result = mBluetoothGattServer.addService(mGattService);
if (result){
Toast.makeText(daqiActivity.this,"新增服務成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(daqiActivity.this,"新增服務失敗",Toast.LENGTH_SHORT).show();
}
複製程式碼
定義Gatt Server回撥。當中心裝置連線該手機外設、修改特徵值、讀取特徵值等情況時,會得到相應情況的回撥。
private class daqiBluetoothGattServerCallback extends BluetoothGattServerCallback{
//裝置連線/斷開連線回撥
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
super.onConnectionStateChange(device, status, newState);
}
//新增本地服務回撥
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
super.onServiceAdded(status, service);
}
//特徵值讀取回撥
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
}
//特徵值寫入回撥
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
}
//描述讀取回撥
@Override
public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
super.onDescriptorReadRequest(device, requestId, offset, descriptor);
}
//描述寫入回撥
@Override
public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
}
}
複製程式碼
最後開啟廣播後,用nRF連線後看到的特徵值資訊如下圖所示:(加多了一個只能都的特徵值)