React-native BlE蓝牙连接打印机

蓝牙知识

在iOS开发中,实现蓝牙通信有两种方式,一种是使用传统的GameKit.framework,另一种就是使用在iOS 5中加入的CoreBluetooth.framework。
BLE分为三部分Service(服务)、Characteristic(特征)、Descriptor(描述符),这三部分都由UUID作为唯一标示符。一个蓝牙4.0的终端可以包含多个Service,一个Service可以包含多个Characteristic,一个Characteristic包含一个Value和多个Descriptor,一个Descriptor包含一个Value。一般来说,Characteristic是手机与BLE终端交换数据的关键,Characteristic有跟权限相关的字段,如Property,Property有读写等各种属性,如Notify、Read、Write、WriteWithoutResponse。(引自: Android BLE开发之Android手机与BLE终端通信 )

利用CoreBluetooth框架,我们可以轻松实现两个iOS设备、iOS设备与非iOS蓝牙设备的交互。要注意的一点是目前这个框架只能支持蓝牙4.0BLE标准,所以对硬件上是有一定要求的,iPhone 4S及以后的设备,第三代iPad及以后的设备是支持这一标准的。

  • 由于项目需要,基于React Native 开发的App要跟BLE蓝牙设备通信。
    js.coach 上搜索React Native BLE蓝牙组件,只找到三个组件:
  • react-native-ble-plx 文档 阅读起来有点难度,但API很丰富,相比 react-native-ble-manager 显的比较专业。(PS:这个组件的ios环境配置比较容易出错,我试用了这个组件并写了一个demo,代码已上传到github : demo源码地址 ) react-native-ble :由Node.js BLE移植而成,而且久未更新,能不能适配最新React Native版本还是个问题,没有深入研究。
    综上分析,我当初选择的是react-native-ble-manager,组件的安装、配置看 官方文档 即可。
    PS:由于 react-native-ble-manager 更新比较频繁,本教程最初是基于3.2.1版本编写,由于版本跨度比较大,导致demo出现了一些问题,现已将代码和文章教程全部更新到6.2.4版本,如后面的版本没有及时更新适配,自己也可以根据 官方文档 作出相对应的更改,但通信原理和步骤是不变的。
    import BleManager from 'react-native-blemanager';
    const BleManagerModule = NativeModules.BleManager;
    const bleManagerEmitter = new NativeEventEmitter(BleManagerModule);
    

    初步运行-然后打开蓝牙-点击打开scan Bluetooth

    //扫描可用设备,5秒后结束 
    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);
    

    扫描后的列表

    //搜索到一个新设备监听
    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地址,但跨平台识别是否是同一设备时需要Mac地址
            //如果广播携带有Mac地址,ios可通过广播0x18获取蓝牙Mac地址,
            macAddress = getMacAddressFromIOS(data);
            id = data.id;
    //搜索结束监听
    bleManagerEmitter.addListener('BleManagerStopScan', () => {
         console.log('BleManagerStopScan:','Scanning is stopped');      
        //搜索结束后,获取搜索到的蓝牙设备列表,如监听了BleManagerDiscoverPeripheral,可省去这个步骤
        BleManager.getDiscoveredPeripherals([])
           .then((peripheralsArray) => {
               console.log('Discovered peripherals: ', peripheralsArray);
    /** ios系统从蓝牙广播信息中获取蓝牙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);
    

    添加相应的监听器

    //蓝牙设备已连接监听
    bleManagerEmitter.addListener('BleManagerConnectPeripheral', (args) => {
        log('BleManagerConnectPeripheral:', args);
    //蓝牙设备已断开连接监听
    bleManagerEmitter.addListener('BleManagerDisconnectPeripheral', (args) => {
        console.log('BleManagerDisconnectPeripheral:', args);
    

    蓝牙连接后会显示该设备的具体信息,android平台下连接成功后返回的数据如下:

    { 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平台下连接成功后返回的数据如下:

    { 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(服务)、Characteristic(特征)、Descriptor(描述符),这三部分都由UUID作为唯一标示符。一个蓝牙4.0的终端可以包含多个Service,一个Service可以包含多个Characteristic,一个Characteristic包含一个Value和多个Descriptor,一个Descriptor包含一个Value。一般来说,Characteristic是手机与BLE终端交换数据的关键,Characteristic有跟权限相关的字段,如Property,Property有读写等各种属性,如Notify、Read、Write、WriteWithoutResponse。(引自:Android BLE开发之Android手机与BLE终端通信)

    Service

    一个低功耗蓝牙设备可以定义多个Service, Service可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:
    0x0000xxxx-0000-1000-8000-00805F9B34FB
    为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的”x”部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:
    0x00002A37-0000-1000-8000-00805F9B34FB(引自:Android BLE 蓝牙开发入门

    Characteristic

    在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。(引自:Android BLE 蓝牙开发入门

    蓝牙连接成功后,需要调用retrieveServices方法获取NotifyReadWriteserviceUUIDcharacteristicUUID作为参数来跟蓝牙进一步通信

    //获取蓝牙Service和Characteristics
    BleManager.retrieveServices(peripheralId)
        .then((peripheralInfo) => {
            this.getUUID();
            console.log('Peripheral info:', peripheralInfo);
    

    peripheralInfo下的characteristics字段值是一个特征数组,每一项代表一个特征通道,找到properties中包含有Notify、Read、Write、WriteWithoutResponse属性的那一项,其service和characteristic即是我们需要的参数。
    PS:serviceUUID和characteristicUUID标准格式为XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX的128bit的UUID。所以需要将获到的’XXXX’格式的UUID转换为标准的128bit的UUID格式才可能进行通信。
    不同的蓝牙设备,可能有多个特征通道包含Notify、Read、Write、WriteWithoutResponse属性值,那每个通道属性的功能可能会不一样,应根据具体的蓝牙设备选择符合我们要求的特征通道。有些可能不包含Notify、Read、Write、WriteWithoutResponse中的一个或多个属性,具体跟蓝牙硬件有关系,一般有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()
    

    蓝牙连接成功,当我们向设备写入数据成功并且指令也正确的话,我们就会得到设备通过蓝牙发送给APP的响应数据,实现这一响应的前提是需要开启通知监听,这样就能在回调中监听到数据返回了。

    //打开通知
    BleManager.startNotification(peripheralId, this.nofityServiceUUID[0], this.nofityCharacteristicUUID[0])
        .then(() => {
            console.log('Notification started');
        .catch((error) => {
            console.log('Notification error:',error);
    //关闭通知
    BleManager.stopNotification(peripheralId, this.nofityServiceUUID[0], this.nofityCharacteristicUUID[0])
       .then(() => {
            console.log('stopNotification success!');
        .catch((error) => {
            console.log('stopNotification error:',error);
    

    添加相应的监听器

    //接收到新数据监听,开启通知成功后,该监听才可接收到数据
    bleManagerEmitter.addListener('BleManagerDidUpdateValueForCharacteristic', (data) => {
        //ios接收到的是小写的16进制,android接收的是大写的16进制,统一转化为大写16进制
        let value = data.value.toUpperCase();               
        console.log('BluetoothUpdateValue', value);
    
    //读取蓝牙数据
    BleManager.read(peripheralId, this.readServiceUUID[0], this.readCharacteristicUUID[0])
         .then((data) => {
             console.log('Read: ',data);                    
         .catch((error) => {
             console.log(error);
    

    写数据有两个方法,write和writeWithoutResponse,大部分蓝牙都有write属性,而writeWithoutResponse属性比较少。

    写数据注意事项:

    BLE蓝牙传输速率比经典蓝牙慢的多,而且GATT底层需要封装7个字节的额外协议数据, 即一次最多只能传输20字节,所以一般选用16进制数据来提高单次数据传输量。而且如果发送的数据大于20字节的话要分包发送,例如要发送30个字节,可以先write(前20个字节),等这次写入成功后(或者开启线程sleep几十毫秒后),再write(后面10个字节)。

    发送的时候需要先将其装载到byte[]数组中,例如要发送FE FD 01 0A FC FB这个指令,需要把它转化为
    new byte[] { (byte) 0xFE,(byte) 0xFD,0x01,0x0A,(byte) 0xFC,(byte) 0xFB }
    这样去发送。

    这是官方最新的例子
    import { stringToBytes } from 'convert-string';
    //发送给蓝牙的指令
    let command = 'FEFD010AFCFB'; 
    //将字符串转换成字节数组传送,stringToByte方法将每个16进制的字符转换成指定位置的字符的 Unicode编码,这个返回值是 0 - 65535 之间的整数
    let bytes = stringToBytes(command);  
    // 转换后为:[ 70, 69, 70, 68, 48, 49, 48, 65, 70, 67, 70, 66 ]
    

    5.0.1之前的版本写数据是需要经过base64编码转换后发送的,5.0.1之后的版本虽然能发送byte[],但却是通过stringToBytes将其转化为Unicode编码位置的byte[],然而蓝牙那边只能接收16进制的byte[]数据。带着这个疑问,我提了一个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文件。
    点击跳转到修改后的BleManager.java文件

    BleManager.java文件

  • 增加的方法
  • /** 16进制字符串转换成16进制byte数组,每两位转换 */
    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;
    

    写到这里 蓝牙打印就已经成功了 贴出我所有的代码,蓝牙打印不需要配对 连接上了直接发送指令就可以了

    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);