03 蓝牙设备开发指南 - MiEcosystem/miot-plugin-sdk GitHub Wiki

蓝牙设备开发指南

目的

本文档面向米家扩展程序开发人员,意在阐述接入蓝牙设备须遵守的协议,以及米家 APP 接口使用。

如非特殊说明,本文档涉及的蓝牙设备均指支持 BLE 4.0+ 设备。

阅读本文档前请确保:

  1. 已在小米 小米IoT开发者平台 注册并通过相关资质;
  2. 已创建蓝牙硬件,并通过审核,硬件状态为"开发中"。
  3. RN开发环境均已按照开发简介正确配置

米家协议

我们称之为 Mi Beacon。未被连接的设备需广播 Mi Beacon,以便于于用户使用米家 APP 发现、连接设备。同时,设备可在 Mi Beacon 中包含有效数据信息,米家 APP 以及蓝牙网关可通过扫描,在不建立连接的情况下直接获取信息,展示给用户。

MiBeacon 定义:

  • Advertising 中 Service Data(0x16) 含有 Mi Service (0xFE95);

  • Scan Response 中 Manufacturer Specific Data(0xFF) 含有小米公司识别码(0x038F);

  • 按以下表格所列格式构造内容:

    字段名称 类型 长度 强制 说明
    Frame Control Bitmap 2 控制位
    Product ID U16 2 产品唯一ID
    Frame Counter U8 1 序号(用于去重)
    MAC Address U8 6 设备MAC地址
    Capability U8 1 设备能力
    I/O Capability U8 2 Capability扩展,用于标识输入、输出能力
    Object U8 N 事件或属性
    Random Number/高位Frame Counter U8 3 若使用加密则为必选字段
    Message Integrity Check U8 1 or 4 若使用加密则为必选字段
    Mesh U8 2 MESH状态及能力

    更详细的格式说明参考:米家BLE广播协议

    米家 APP 在发现设备时会解析上述所有字段,用于识别设备。比如通过 Product ID 从小米 IoT 开发者平台查询设备的 Model 以及默认名称;通过 Mac Address 字段获取设备 Mac 地址,尤其是 iOS 客户端,由于系统的封闭性,只能通过 Mi Beacon 获取,故务必保证填充的数据的准确性;通过 Frame Control 中的Binding Confirm 变化主动连接设备等等。

    扩展程序开发人员可在必要时通过扫描工具抓取设备发出的广播,Debug 对应字段是否符合预期来排除错误,如 nRF Connect

安全认证

出于安全原因,米家 APP(Central)与设备(Peripheral) 建立连接,不仅需要建立常规 BLE 连接,还需要进行一系列数据交换与运算,以保证彼此信任以及交换加密密钥。我们定义该过程为安全认证。安全认证可分为两部分:

  1. 绑定

    可理解为在米家 APP 中添加该设备至用户小米账号的过程,由米家完成,由用户进入米家添加设备功能模块、按引导连接某个蓝牙设备触发。扩展程序开发者对该部分无开发工作。

  2. 连接

    对于已绑定的设备,米家APP再次建立可信连接的过程。需要调用Bluetooth模块与蓝牙设备交互。流程如下:

    1. import对应的module,定义全局变量bt
      import { Bluetooth, BluetoothEvent, Device } from "miot";
      import Host from "miot/Host";
    
      let bt = undefined;
    
    1. 在componentDidMount加入如下代码验证蓝牙权限
      Bluetooth.checkBluetoothIsEnabled().then(result => {
          this.state.isEnable = result;
          if (result) {
              console.log("蓝牙已开启:",result)
              this.startScan();
          } else {
              console.log("蓝牙未开启,请检查开启蓝牙后再试")
              Host.ui.showBLESwitchGuide();
          }
      });
    
    1. 加上扫描蓝牙设备的方法
      startScan() {
          Bluetooth.startScan(30000, 'FE95','FE96');//扫描指定设备
      }
    
    1. 在componentDidMount加入各种监听的方法,分别为:发现蓝牙设备,发现服务,发现特征值,特征值变化,特征值改变,蓝牙状态变化,设备发现失败,特征值发现失败,蓝牙连接状态变化。开发者应该根据业务需要,在里面加入不同的业务逻辑。
      this._S1 = BluetoothEvent.bluetoothDeviceDiscovered.addListener((result) => {
          if (bt) {
              console.log("发现设备:" + JSON.stringify(result))
          } else {
              console.log("初次发现设备:",JSON.stringify(result))
              //普通蓝牙设备的连接必须在扫描到设备之后手动创建 ble 对象
              bt = Bluetooth.createBluetoothLE(result.uuid || result.mac);//android 用 mac 创建设备,ios 用 uuid 创建设备
              Bluetooth.stopScan();
              this.connect();
          }
      })
      this._s2 = BluetoothEvent.bluetoothSeviceDiscovered.addListener((blut, services) => {
          if (services.length <= 0) { return }
          console.log("蓝牙服务发现完成:",JSON.stringify(services.map(s => s.UUID)))
          if (bt.isConnected) {
              services.forEach(s => {
                console.log("服务:",s)
                console.log("开始扫描特征值")
                s.startDiscoverCharacteristics()
              })
          }
      })
      this._s3 = BluetoothEvent.bluetoothCharacteristicDiscovered.addListener((bluetooth, service, characters) => {
          console.log("蓝牙特征值发现:", characters.map(s => s.UUID)+ bt.isConnected);
      })
      this._s4 = BluetoothEvent.bluetoothCharacteristicValueChanged.addListener((bluetooth, service, character, value) => {
          if (service.UUID.indexOf("ffd5") > 0) {
              console.log("ffd5的特征值更改:", character.UUID+value);
          }
          console.log("特征值更改:", character.UUID + ">" + value)
      })
      this._s5 = BluetoothEvent.bluetoothStatusChanged.addListener((data) => {
          if (data) {
              console.log("蓝牙状态发生变化:", JSON.stringify(data))
          } else {
            console.log("蓝牙状态发生变化:", data);
          }
      });
      this._s6 = BluetoothEvent.bluetoothSeviceDiscoverFailed.addListener((blut, data) => {
          console.log("服务发现失败:", data);
      })
      this._s7 = BluetoothEvent.bluetoothCharacteristicDiscoverFailed.addListener((blut, data) => {
          console.log("特征值发现失败:", data);
      })
      this._s8 = BluetoothEvent.bluetoothConnectionStatusChanged.addListener((blut, isConnect) => {
          console.log("蓝牙连接状态改变:", blut, isConnect);
      })
    
    记得在componentWillUnmount方法中,断开蓝牙连接,并且remove掉这些监听。
    
      componentWillUnmount() {
          if (bt && bt.isConnected) {
              bt.disconnect();
              console.log("disconnect");
          }
          this._s1.remove();
          this._s2.remove();
          this._s3.remove();
          this._s4.remove();
          this._s5.remove();
          this._s6.remove();
      }
    
    1. 实现connect方法
      connect() {
          console.log("准备开始蓝牙连接")
          if (bt.isConnected) {
              console.log("蓝牙设备已经连接,开始发现服务")
              bt.startDiscoverServices();
          } else if (bt.isConnecting) {
              console.log("蓝牙正处于连接中,请等待连接结果后再试")
          } else {
              bt.connect(-1).then((data) => {
                  console.log("蓝牙连接成功: ", JSON.stringify(data))
                  console.log("蓝牙设备已经连接,开始发现服务")
                  bt.startDiscoverServices();
              }).catch((data) => {
                  console.log("蓝牙连接失败: ", JSON.stringify(data))
                  // 如果是小米协议蓝牙设备,才需要判断data.code === 7,否则不需要下面这一段
                  if (data.code === 7) {
                      Bluetooth.retrievePeripheralsWithServicesForIOS('serviceid1', 'serviceid2').then(res => {
                        // todo
                      })
                  }
              });
          }
      }
    
    在retrievePeripheralsWithServicesForIOS里面的todo位置,可以获取到已经连接的蓝牙对象,小米协议设备返回7,大几率是本身已经连接,在这里可以选择:
    
    • 获取到蓝牙的uuid,通过普通蓝牙对象操作 let ble = Bluetooth.createBluetoothLE(result.uuid || result.mac) ble.connect(3).then(...)

    • 获取到蓝牙uuid, 之后disconnect, 随后再使用小米协议的连接方式 let ble = Bluetooth.createBluetoothLE(result.uuid || result.mac) ble.disconnect() ble.connect(-1).then(...)

上述步骤会尝试搜索并连接已绑定设备,完成认证过程,并且会在连接成功后,按需发现 GATT 服务与特征值、读写数据。

安全认证过程全部的数据交换在 Mi Service(0xFE95) 上进行。

设备固件开发人员支持安全认证,请参考嵌入式文档:mijia_blemijia_ble_secure 。若有其他蓝牙相关问题,请移步 issues搜索查看,此处不再赘述。

固件升级

米家app集成了nordic固件升级库"react-native-nordic-dfu",极大的方便开发者自行遵循官方方案实现OTA升级,且运行稳定可靠。 具体使用方法请参考github地址

相比较下,JS调用RN下的蓝牙模块接口去做固件升级,实现起来繁琐困难,且容易出bug,推荐nordic库方案。

nordic库升级过程中,可能会遇到的问题详见附录。

锁类产品

锁类产品和上面所讲的蓝牙产品最根本的区别是安全等级较高,安全方面的问题,已经由嵌入式工程师和米家这边处理完毕,插件工程师只需注意以下几个功能的使用即可:

开锁/关锁

  Bluetooth.createBluetoothLE(...).connect(...).then(device => {
   device.securityLock().toggle(0,5000)
       .then(lock => {console.log('toggle success')})
       .catch(err => {console.log('toggle failed'})
  })

加密/解密

 Bluetooth.createBluetoothLE(...).securityLock().encryptMessage('message')
    .then(msg => {console.log('encrypted message is ', msg)})
    .catch(err => {console.log('encrypted message failed, ', err})

 Bluetooth.createBluetoothLE(...).securityLock().encryptMessage('decryptedMessage')
    .then(msg => {console.log('decrypt message is ', msg)})
    .catch(err => {console.log('decrypt message failed, ', err})

一次性电子钥匙

 Bluetooth.createBluetoothLE(...).securityLock().getOneTimePassword(30,6)
   .then(pwd => {console.log('one time password is ', pwd)})
   .catch(err => {console.log('get one time password failed, ', err})

附录:nordic库升级常见问题

1.设备不支持dfu
问题描述: code:DFUErrorDeviceNotSupported domain:"RCTErrorDomain"

解决方案: 原因:设备蓝牙连接成功后,会得到一个uuidA;部分厂商的硬件设备,进入DFU模式后,设备的uuid会更新成uuidB,此时start DFU应该使用uuidB,如果用了uuidA,就会导致此异常。 推荐解决方案:start DFU之前,执行一次蓝牙scan,通过扫描到的名称和广播里的数据找到自己的设备,确定uuid,start DFU时使用此uuid即可

2.DFU文件找不到
问题描述: 升级中遇到如下错误

{“framesZToPop”:1,”code”:”4097”} miot-rn-plugin: rn-nodrc-dfu onError,deviceAddress:E2:FB:69:C3:EE:15 error: 4097 errorType:0 message:DFU FILE NOT FOUND

解决方案: DFU FILE NOT FOUND错误出现的原因是startDFU时传入的固件升级包filepath不对

  • 下载下来的zip包内容是固件端指定的,是直接使用此.zip,还是使用解压后包里另一个.zip当固件升级包(filepath对应的文件),这个需要咨询固件端
  • 此filepath是指向固件升级包文件(.zip)的全路径
  • 请确保filePath指向的本地文件确实存在
  • 请确保固件升级包文件名必须是固件端能认的,如果文件名错了(即使文件内容是对的)也可能导致此错误

3.DFU文件不合法
问题描述: 升级中遇到如下错误

miot-rn-plugin: rn-nodrc-dfu onError, deviceAddress: E2:FB:69:C3:EE:15 error: 4098 errorType:0 message:DFU FILE ERROR

解决方案: 老版米家app存在此问题,最新版app已修复

4.问题4 DFU过程中蓝牙断连
问题描述: dfu过程有一定概率遇到 DFUSTATE 没有 SUCCESS 就直接断开连接了,这个时候其实 DFU 已经成功了

解决方案: 可以在某个地方记录上传的记录,如果上传到过100% ,通常后面的那个断开连接基本上就是因为DFU成功导致,以此容错。如果测试多次没遇到这个情况可以不用做这个,可能和设备使用的 Nordic 版本有关。

如果还有其它问题,可查阅其它在线文档,也可在github提issue给米家

Pilloxa/react-native-nordic-dfu issues

Android DFU library issues

iOS DFU library issues

⚠️ **GitHub.com Fallback** ⚠️