藍牙知識
在iOS開發(fā)中,實現(xiàn)藍牙通信有兩種方式,一種是使用傳統(tǒng)的GameKit.framework,另一種就是使用在iOS 5中加入的CoreBluetooth.framework。
BLE分為三部分Service(服務(wù))、Characteristic(特征)、Descriptor(描述符),這三部分都由UUID作為唯一標(biāo)示符。一個藍牙4.0的終端可以包含多個Service,一個Service可以包含多個Characteristic,一個Characteristic包含一個Value和多個Descriptor,一個Descriptor包含一個Value。一般來說,Characteristic是手機與BLE終端交換數(shù)據(jù)的關(guān)鍵,Characteristic有跟權(quán)限相關(guān)的字段,如Property,Property有讀寫等各種屬性,如Notify、Read、Write、WriteWithoutResponse。(引自:Android BLE開發(fā)之Android手機與BLE終端通信)
利用CoreBluetooth框架,我們可以輕松實現(xiàn)兩個iOS設(shè)備、iOS設(shè)備與非iOS藍牙設(shè)備的交互。要注意的一點是目前這個框架只能支持藍牙4.0BLE標(biāo)準(zhǔn),所以對硬件上是有一定要求的,iPhone 4S及以后的設(shè)備,第三代iPad及以后的設(shè)備是支持這一標(biāo)準(zhǔn)的。
- 由于項目需要,基于React Native 開發(fā)的App要跟BLE藍牙設(shè)備通信。
在js.coach上搜索React Native BLE藍牙組件,只找到三個組件: -
react-native-ble-plx:文檔閱讀起來有點難度,但API很豐富,相比
react-native-ble-manager顯的比較專業(yè)。(PS:這個組件的ios環(huán)境配置比較容易出錯,我試用了這個組件并寫了一個demo,代碼已上傳到github : demo源碼地址)
-
react-native-ble-plx:文檔閱讀起來有點難度,但API很豐富,相比
-
react-native-ble:由Node.js BLE移植而成,而且久未更新,能不能適配最新React Native版本還是個問題,沒有深入研究。
綜上分析,我當(dāng)初選擇的是react-native-ble-manager,組件的安裝、配置看官方文檔即可。
PS:由于react-native-ble-manager更新比較頻繁,本教程最初是基于3.2.1版本編寫,由于版本跨度比較大,導(dǎo)致demo出現(xiàn)了一些問題,現(xiàn)已將代碼和文章教程全部更新到6.2.4版本,如后面的版本沒有及時更新適配,自己也可以根據(jù)官方文檔作出相對應(yīng)的更改,但通信原理和步驟是不變的。
image.png
-
react-native-ble:由Node.js BLE移植而成,而且久未更新,能不能適配最新React Native版本還是個問題,沒有深入研究。
項目初始化
import BleManager from 'react-native-blemanager';
const BleManagerModule = NativeModules.BleManager;
const bleManagerEmitter = new NativeEventEmitter(BleManagerModule);
初步運行-然后打開藍牙-點擊打開scan Bluetooth
//掃描可用設(shè)備,5秒后結(jié)束
BleManager.scan([], 5, true)
.then(() => {
console.log('Scan started');
})
.catch( (err)=>{
console.log('Scan started fail');
});
//停止掃描
BleManager.stopScan()
.then(() => {
console.log('Scan stopped');
})
.catch( (err)=>{
console.log('Scan stopped fail',err);
});
掃描后的列表

這時候就是選擇想連接的藍牙了
首先我們要添加相應(yīng)的監(jiān)聽器
//搜索到一個新設(shè)備監(jiān)聽
bleManagerEmitter.addListener('BleManagerDiscoverPeripheral', (data) => {
console.log('BleManagerDiscoverPeripheral:', data);
let id; //藍牙連接id
let macAddress; //藍牙Mac地址
if(Platform.OS == 'android'){
macAddress = data.id;
id = macAddress;
}else{
//ios連接時不需要用到Mac地址,但跨平臺識別是否是同一設(shè)備時需要Mac地址
//如果廣播攜帶有Mac地址,ios可通過廣播0x18獲取藍牙Mac地址,
macAddress = getMacAddressFromIOS(data);
id = data.id;
}
});
//搜索結(jié)束監(jiān)聽
bleManagerEmitter.addListener('BleManagerStopScan', () => {
console.log('BleManagerStopScan:','Scanning is stopped');
//搜索結(jié)束后,獲取搜索到的藍牙設(shè)備列表,如監(jiān)聽了BleManagerDiscoverPeripheral,可省去這個步驟
BleManager.getDiscoveredPeripherals([])
.then((peripheralsArray) => {
console.log('Discovered peripherals: ', peripheralsArray);
});
});
/** ios系統(tǒng)從藍牙廣播信息中獲取藍牙MAC地址 */
getMacAddressFromIOS(data){
let macAddressInAdvertising = data.advertising.kCBAdvDataManufacturerMacAddress;
//為undefined代表此藍牙廣播信息里不包括Mac地址
if(!macAddressInAdvertising){
return;
}
macAddressInAdvertising = macAddressInAdvertising.replace("<","").replace(">","").replace(" ","");
if(macAddressInAdvertising != undefined && macAddressInAdvertising != null && macAddressInAdvertising != '') {
macAddressInAdvertising = swapEndianWithColon(macAddressInAdvertising);
}
return macAddressInAdvertising;
}
/**
* ios從廣播中獲取的mac地址進行大小端格式互換,并加上冒號:
* @param str 010000CAEA80
* @returns string 80:EA:CA:00:00:01
*/
swapEndianWithColon(str){
let format = '';
let len = str.length;
for(let j = 2; j <= len; j = j + 2){
format += str.substring(len-j, len-(j-2));
if(j != len) {
format += ":";
}
}
return format.toUpperCase();
}
然后就是連接
android使用Mac地址與藍牙連接,ios使用UUID與藍牙連接
//連接藍牙
BleManager.connect(id)
.then(() => {
console.log('Connected');
})
.catch((error) => {
console.log('Connected error:',error);
});
//斷開藍牙連接
BleManager.disconnect(id)
.then( () => {
console.log('Disconnected');
})
.catch( (error) => {
console.log('Disconnected error:',error);
});
添加相應(yīng)的監(jiān)聽器
//藍牙設(shè)備已連接監(jiān)聽
bleManagerEmitter.addListener('BleManagerConnectPeripheral', (args) => {
log('BleManagerConnectPeripheral:', args);
});
//藍牙設(shè)備已斷開連接監(jiān)聽
bleManagerEmitter.addListener('BleManagerDisconnectPeripheral', (args) => {
console.log('BleManagerDisconnectPeripheral:', args);
});

藍牙連接后會顯示該設(shè)備的具體信息,android平臺下連接成功后返回的數(shù)據(jù)如下:
{ characteristics:
[ { properties: { Read: 'Read' },
characteristic: '2a00',
service: '1800' },
{ properties: { Read: 'Read' },
characteristic: '2a01',
service: '1800' },
{ properties: { Write: 'Write', Read: 'Read' },
characteristic: '2a02',
service: '1800' },
{ properties: { Read: 'Read' },
characteristic: '2a04',
service: '1800' },
{ descriptors: [ { value: null, uuid: '2902' } ],
properties: { Indicate: 'Indicate', Read: 'Read' },
characteristic: '2a05',
service: '1801' },
{ descriptors: [ { value: null, uuid: '2902' }, { value: null, uuid: '2901' } ],
properties: { Notify: 'Notify' },
characteristic: '0783b03e-8535-b5a0-7140-a304d2495cb8',
service: '0783b03e-8535-b5a0-7140-a304d2495cb7' },
{ descriptors: [ { value: null, uuid: '2902' }, { value: null, uuid: '2901' } ],
properties: { WriteWithoutResponse: 'WriteWithoutResponse' },
characteristic: '0783b03e-8535-b5a0-7140-a304d2495cba',
service: '0783b03e-8535-b5a0-7140-a304d2495cb7' },
{ descriptors: [ { value: null, uuid: '2902' }, { value: null, uuid: '2901' } ],
properties:
{ Notify: 'Notify',
WriteWithoutResponse: 'WriteWithoutResponse',
Read: 'Read' },
characteristic: '0783b03e-8535-b5a0-7140-a304d2495cb9',
service: '0783b03e-8535-b5a0-7140-a304d2495cb7' } ],
services:
[ { uuid: '1800' },
{ uuid: '1801' },
{ uuid: '0783b03e-8535-b5a0-7140-a304d2495cb7' } ],
rssi: -46,
advertising:{ data: 'AgEGEQe3XEnSBKNAcaC1NYU+sIMHCQlQRVAtUEVOLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=',CDVType: 'ArrayBuffer' },
id: '00:CD:FF:00:22:2D',
name: 'PEP-HC001'
}
ios平臺下連接成功后返回的數(shù)據(jù)如下:
{ name: 'PEP-HC001',
id: '64319987-E97B-46C0-91AE-261E93EADBFD',
advertising:
{ kCBAdvDataLocalName: 'PEP-HC001',
kCBAdvDataIsConnectable: true,
kCBAdvDataServiceUUIDs: [ '0783' ],
kCBAdvDataManufacturerMacAddress: '<472200ff cd00>',
kCBAdvDataManufacturerData: { CDVType: 'ArrayBuffer', data: 'RyIA/80A' } },
services: [ '0783B03E-8535-B5A0-7140-A304D2495CB7' ],
characteristics:
[ { service: '0783B03E-8535-B5A0-7140-A304D2495CB7',
isNotifying: false,
characteristic: '0783B03E-8535-B5A0-7140-A304D2495CB8',
properties: [ 'Notify' ] },
{ service: '0783B03E-8535-B5A0-7140-A304D2495CB7',
isNotifying: false,
characteristic: '0783B03E-8535-B5A0-7140-A304D2495CBA',
properties: [ 'WriteWithoutResponse' ] },
{ service: '0783B03E-8535-B5A0-7140-A304D2495CB7',
isNotifying: false,
characteristic: '0783B03E-8535-B5A0-7140-A304D2495CB9',
properties: [ 'Read', 'WriteWithoutResponse', 'Notify' ] } ],
rssi: -35 }
獲取Service和Characteristic
BLE分為三部分Service(服務(wù))、Characteristic(特征)、Descriptor(描述符),這三部分都由UUID作為唯一標(biāo)示符。一個藍牙4.0的終端可以包含多個Service,一個Service可以包含多個Characteristic,一個Characteristic包含一個Value和多個Descriptor,一個Descriptor包含一個Value。一般來說,Characteristic是手機與BLE終端交換數(shù)據(jù)的關(guān)鍵,Characteristic有跟權(quán)限相關(guān)的字段,如Property,Property有讀寫等各種屬性,如Notify、Read、Write、WriteWithoutResponse。(引自:Android BLE開發(fā)之Android手機與BLE終端通信)
Service
一個低功耗藍牙設(shè)備可以定義多個Service, Service可以理解為一個功能的集合。設(shè)備中每一個不同的 Service 都有一個 128 bit 的 UUID 作為這個 Service 的獨立標(biāo)志。藍牙核心規(guī)范制定了兩種不同的UUID,一種是基本的UUID,一種是代替基本UUID的16位UUID。所有的藍牙技術(shù)聯(lián)盟定義UUID共用了一個基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
為了進一步簡化基本UUID,每一個藍牙技術(shù)聯(lián)盟定義的屬性有一個唯一的16位UUID,以代替上面的基本UUID的”x”部分。例如,心率測量特性使用0X2A37作為它的16位UUID,因此它完整的128位UUID為:
0x00002A37-0000-1000-8000-00805F9B34FB(引自:Android BLE 藍牙開發(fā)入門)
Characteristic
在 Service 下面,又包括了許多的獨立數(shù)據(jù)項,我們把這些獨立的數(shù)據(jù)項稱作 Characteristic。同樣的,每一個 Characteristic 也有一個唯一的 UUID 作為標(biāo)識符。建立藍牙連接后,我們說的通過藍牙發(fā)送數(shù)據(jù)給外圍設(shè)備就是往這些 Characteristic 中的 Value 字段寫入數(shù)據(jù);外圍設(shè)備發(fā)送數(shù)據(jù)給手機就是監(jiān)聽這些 Charateristic 中的 Value 字段有沒有變化,如果發(fā)生了變化,手機的 BLE API 就會收到一個監(jiān)聽的回調(diào)。(引自:Android BLE 藍牙開發(fā)入門)
藍牙連接成功后,需要調(diào)用retrieveServices方法獲取Notify、Read、Write的serviceUUID和characteristicUUID作為參數(shù)來跟藍牙進一步通信
//獲取藍牙Service和Characteristics
BleManager.retrieveServices(peripheralId)
.then((peripheralInfo) => {
this.getUUID();
console.log('Peripheral info:', peripheralInfo);
});
peripheralInfo下的characteristics字段值是一個特征數(shù)組,每一項代表一個特征通道,找到properties中包含有Notify、Read、Write、WriteWithoutResponse屬性的那一項,其service和characteristic即是我們需要的參數(shù)。
PS:serviceUUID和characteristicUUID標(biāo)準(zhǔn)格式為XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX的128bit的UUID。所以需要將獲到的’XXXX’格式的UUID轉(zhuǎn)換為標(biāo)準(zhǔn)的128bit的UUID格式才可能進行通信。
不同的藍牙設(shè)備,可能有多個特征通道包含Notify、Read、Write、WriteWithoutResponse屬性值,那每個通道屬性的功能可能會不一樣,應(yīng)根據(jù)具體的藍牙設(shè)備選擇符合我們要求的特征通道。有些可能不包含Notify、Read、Write、WriteWithoutResponse中的一個或多個屬性,具體跟藍牙硬件有關(guān)系,一般有Notify和Write兩個屬性就可以滿足通信的要求了。
//獲取Notify、Read、Write、WriteWithoutResponse的serviceUUID和characteristicUUID
getUUID(peripheralInfo){
this.readServiceUUID = [];
this.readCharacteristicUUID = [];
this.writeWithResponseServiceUUID = [];
this.writeWithResponseCharacteristicUUID = [];
this.writeWithoutResponseServiceUUID = [];
this.writeWithoutResponseCharacteristicUUID = [];
this.nofityServiceUUID = [];
this.nofityCharacteristicUUID = [];
for(let item of peripheralInfo.characteristics){
item.service = this.fullUUID(item.service);
item.characteristic = this.fullUUID(item.characteristic);
if(Platform.OS == 'android'){
if(item.properties.Notify == 'Notify'){
this.nofityServiceUUID.push(item.service);
this.nofityCharacteristicUUID.push(item.characteristic);
}
if(item.properties.Read == 'Read'){
this.readServiceUUID.push(item.service);
this.readCharacteristicUUID.push(item.characteristic);
}
if(item.properties.Write == 'Write'){
this.writeWithResponseServiceUUID.push(item.service);
this.writeWithResponseCharacteristicUUID.push(item.characteristic);
}
if(item.properties.Write == 'WriteWithoutResponse'){
this.writeWithoutResponseServiceUUID.push(item.service);
this.writeWithoutResponseCharacteristicUUID.push(item.characteristic);
}
}else{ //ios
for(let property of item.properties){
if(property == 'Notify'){
this.nofityServiceUUID.push(item.service);
this.nofityCharacteristicUUID.push(item.characteristic);
}
if(property == 'Read'){
this.readServiceUUID.push(item.service);
this.readCharacteristicUUID.push(item.characteristic);
}
if(property == 'Write'){
this.writeWithResponseServiceUUID.push(item.service);
this.writeWithResponseCharacteristicUUID.push(item.characteristic);
}
if(property == 'WriteWithoutResponse'){
this.writeWithoutResponseServiceUUID.push(item.service);
this.writeWithoutResponseCharacteristicUUID.push(item.characteristic);
}
}
}
}
}
/**
* Converts UUID to full 128bit.
*
* @param {UUID} uuid 16bit, 32bit or 128bit UUID.
* @returns {UUID} 128bit UUID.
*/
fullUUID(uuid) {
if (uuid.length === 4){
return '0000' + uuid.toUpperCase() + '-0000-1000-8000-00805F9B34FB'
}
if (uuid.length === 8) {
return uuid.toUpperCase() + '-0000-1000-8000-00805F9B34FB'
}
return uuid.toUpperCase()
}
通知監(jiān)聽
藍牙連接成功,當(dāng)我們向設(shè)備寫入數(shù)據(jù)成功并且指令也正確的話,我們就會得到設(shè)備通過藍牙發(fā)送給APP的響應(yīng)數(shù)據(jù),實現(xiàn)這一響應(yīng)的前提是需要開啟通知監(jiān)聽,這樣就能在回調(diào)中監(jiān)聽到數(shù)據(jù)返回了。
//打開通知
BleManager.startNotification(peripheralId, this.nofityServiceUUID[0], this.nofityCharacteristicUUID[0])
.then(() => {
console.log('Notification started');
})
.catch((error) => {
console.log('Notification error:',error);
});
//關(guān)閉通知
BleManager.stopNotification(peripheralId, this.nofityServiceUUID[0], this.nofityCharacteristicUUID[0])
.then(() => {
console.log('stopNotification success!');
})
.catch((error) => {
console.log('stopNotification error:',error);
});
添加相應(yīng)的監(jiān)聽器
//接收到新數(shù)據(jù)監(jiān)聽,開啟通知成功后,該監(jiān)聽才可接收到數(shù)據(jù)
bleManagerEmitter.addListener('BleManagerDidUpdateValueForCharacteristic', (data) => {
//ios接收到的是小寫的16進制,android接收的是大寫的16進制,統(tǒng)一轉(zhuǎn)化為大寫16進制
let value = data.value.toUpperCase();
console.log('BluetoothUpdateValue', value);
});
讀數(shù)據(jù)
//讀取藍牙數(shù)據(jù)
BleManager.read(peripheralId, this.readServiceUUID[0], this.readCharacteristicUUID[0])
.then((data) => {
console.log('Read: ',data);
})
.catch((error) => {
console.log(error);
});
寫數(shù)據(jù)
寫數(shù)據(jù)有兩個方法,write和writeWithoutResponse,大部分藍牙都有write屬性,而writeWithoutResponse屬性比較少。
寫數(shù)據(jù)注意事項:
BLE藍牙傳輸速率比經(jīng)典藍牙慢的多,而且GATT底層需要封裝7個字節(jié)的額外協(xié)議數(shù)據(jù), 即一次最多只能傳輸20字節(jié),所以一般選用16進制數(shù)據(jù)來提高單次數(shù)據(jù)傳輸量。而且如果發(fā)送的數(shù)據(jù)大于20字節(jié)的話要分包發(fā)送,例如要發(fā)送30個字節(jié),可以先write(前20個字節(jié)),等這次寫入成功后(或者開啟線程sleep幾十毫秒后),再write(后面10個字節(jié))。
發(fā)送的時候需要先將其裝載到byte[]數(shù)組中,例如要發(fā)送FE FD 01 0A FC FB這個指令,需要把它轉(zhuǎn)化為
new byte[] { (byte) 0xFE,(byte) 0xFD,0x01,0x0A,(byte) 0xFC,(byte) 0xFB }
這樣去發(fā)送。
這是官方最新的例子
import { stringToBytes } from 'convert-string';
//發(fā)送給藍牙的指令
let command = 'FEFD010AFCFB';
//將字符串轉(zhuǎn)換成字節(jié)數(shù)組傳送,stringToByte方法將每個16進制的字符轉(zhuǎn)換成指定位置的字符的 Unicode編碼,這個返回值是 0 - 65535 之間的整數(shù)
let bytes = stringToBytes(command);
// 轉(zhuǎn)換后為:[ 70, 69, 70, 68, 48, 49, 48, 65, 70, 67, 70, 66 ]
5.0.1之前的版本寫數(shù)據(jù)是需要經(jīng)過base64編碼轉(zhuǎn)換后發(fā)送的,5.0.1之后的版本雖然能發(fā)送byte[],但卻是通過stringToBytes將其轉(zhuǎn)化為Unicode編碼位置的byte[],然而藍牙那邊只能接收16進制的byte[]數(shù)據(jù)。帶著這個疑問,我提了一個issue給作者,問題是:Can I send hexadecimal data to BLE instead of base64 format?然而作者卻沒有
給我一個滿意的解決辦法。所以,自己動手對源碼做一些小小的修改以符合我們的實際需求吧。
android源碼修改
修改的源文件只有一個:在react-native-ble-manager\android\src\main\java\it\innove目錄下的BleManager.java文件。
點擊跳轉(zhuǎn)到修改后的BleManager.java文件
BleManager.java文件
- 增加的方法
/** 16進制字符串轉(zhuǎn)換成16進制byte數(shù)組,每兩位轉(zhuǎn)換 */
public static byte[] strToHexByteArray(String str){
byte[] hexByte = new byte[str.length()/2];
for(int i = 0,j = 0; i < str.length(); i = i + 2,j++){
hexByte[j] = (byte)Integer.parseInt(str.substring(i,i+2), 16);
}
return hexByte;
}
寫到這里 藍牙打印就已經(jīng)成功了 貼出我所有的代碼,藍牙打印不需要配對 連接上了直接發(fā)送指令就可以了
test(peripheral) {
console.log('jinlai')
if (peripheral){
if (peripheral.connected){
BleManager.disconnect(peripheral.id);
}else{
BleManager.connect(peripheral.id).then(() => {
let peripherals = this.state.peripherals;
let p = peripherals.get(peripheral.id);
if (p) {
p.connected = true;
peripherals.set(peripheral.id, p);
this.setState({peripherals});
}
console.log('Connected to ' + peripheral.id);
setTimeout(() => {
/* Test read current RSSI value
BleManager.retrieveServices(peripheral.id).then((peripheralData) => {
console.log('Retrieved peripheral services', peripheralData);
BleManager.readRSSI(peripheral.id).then((rssi) => {
console.log('Retrieved actual RSSI value', rssi);
});
});*/
// Test using bleno's pizza example
// https://github.com/sandeepmistry/bleno/tree/master/examples/pizza
BleManager.retrieveServices(peripheral.id).then((peripheralInfo) => {
this.getUUID(peripheralInfo)
console.log('peripheralInfo =',peripheralInfo);
var service = this.nofityServiceUUID[0];
// var bakeCharacteristic = '11C5ABAA-AC2D-458D-B12E-260DCB629AE4';
var crustCharacteristic = this.nofityCharacteristicUUID[0];
setTimeout(() => {
//打開通知
console.log('peripheral.id ='+peripheral.id)
console.log('bakeCharacteristic='+this.nofityServiceUUID[0])
console.log('crustCharacteristic='+this.nofityCharacteristicUUID[0])
BleManager.startNotification(peripheral.id, this.nofityServiceUUID[0], this.nofityCharacteristicUUID[0]).then(() => {
console.log('Started notification on ' + peripheral.id);
//寫入
setTimeout(() => {
//1.id 2.寫入的UDID
BleManager.write(peripheral.id, this.writeWithResponseServiceUUID[0], this.writeWithResponseCharacteristicUUID[0], [0]).then(() => {
console.log('Writed NORMAL crust');
BleManager.write(peripheral.id, this.writeWithResponseServiceUUID[0], this.writeWithResponseCharacteristicUUID[0], [1,3]).then(() => {
console.log('Writed 351 temperature, the pizza should be BAKED');
/*
var PizzaBakeResult = {
HALF_BAKED: 0,
BAKED: 1,
CRISPY: 2,
BURNT: 3,
ON_FIRE: 4
};*/
});
});
}, 500);
}).catch((error) => {
console.log('Notification error', error);
});
}, 200);
});
}, 900);
}).catch((error) => {
console.log('Connection error', error);
});
}
}
}

看到這里就知道[0],[1,3]是什么意思了把!他代表的是data,數(shù)據(jù)必須以數(shù)組形式 否則會報錯,不懂的 可以私聊筆者
