前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

React Native蓝牙连接心率带设备实战

首页2019-10-02 11:20:12Front-End
RNReact蓝牙

做一个运动记录类的 App,要把用户胸前那条心率带的数据实时读进来,画成曲线,再定时推给后端。听起来只是「连个蓝牙」,真动手才发现 BLE(低功耗蓝牙)这套东西和平时写业务代码完全是两个思路,权限、扫描、广播包、特征值订阅,每一层都有自己的坑。尤其是 Android 上那个「不开定位就扫不到设备」的行为,第一次遇到能让人怀疑人生。

这篇把当时用 react-native-ble-manager 打通心率带的整个流程拆开讲,从初始化一路讲到数据解析,顺带把原代码里几处会漏内存和拼错的地方改掉。文档地址是 https://github.com/innoveit/react-native-ble-manager#methods ,参数细节以它为准。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • BLE 读数据的两条路线,广播扫描和连接订阅,分别适合什么场景
  • react-native-ble-manager 的初始化顺序,start 和 enableBluetooth 谁先谁后
  • 为什么 Android 上不给定位权限就扫不到任何蓝牙设备
  • BleManager.scan 四个参数各自控制什么,scanMode 和 numberOfMatches 怎么调
  • 从广播包的十六进制串里解析电量、心率、步数
  • 定时扫描的写法,以及事件监听不回收会怎么样
  • 原代码里的几处问题和改法
  • 2019 年到现在,Android 蓝牙权限模型发生了什么变化

# 一、先选路线,广播还是连接

BLE 里拿设备数据有两条完全不同的路。搞清楚自己走哪条,后面的代码才不会拧巴。

第一条是「连接后订阅特征值」。流程是扫描发现设备,connect 建立连接,retrieveServices 拿到服务和特征值列表,然后对某个特征值 startNotification,设备每次有新数据就主动推给你。标准的心率服务(Heart Rate Service,UUID 是 180d)就是这么设计的,数据准、延迟低、能双向通信。代价是连接本身有开销,一次只能连有限个设备,断线还得写重连逻辑。

第二条是「只读广播包」。BLE 设备在没被连接的时候会周期性地往外播报一个广播包,里面能塞一小段自定义数据。你只要一直扫描,就能从广播里把这段数据捞出来,全程不建立连接。

原文这个心率带走的是第二条路。

好处很实在。不用管连接状态,不用处理断线重连,一台手机可以同时收多条心率带的数据,做团课那种多人同屏的场景特别合适。代价是数据量受限,广播包能塞的字节数很少,而且推送频率由设备决定,你只能被动等。厂商在广播包里塞了电量、心率、步数三个值,够用了。

所以下面所有代码的核心逻辑就一句话,反复扫描,在扫描回调里解析广播包。

# 二、初始化和事件监听

先看初始化这一段。

// 以下是处理蓝牙设备的逻辑
state = {
    scanning: false,//蓝牙是否在扫描
}

// 初始化蓝牙设备信息
initDevInfo = ()=>{
    //创建对用户的请求,以激活蓝牙
    BleManager.enableBluetooth()
        .then(() => {
            console.log('开启成功');
        })
        .catch((error) => {
            console.log('The user refuse to enable bluetooth');
        });
        
    //初始化设备
    BleManager.start({ showAlert: false }).then(() => {
        console.log('开始了')
    });
    this.onCheckLocation();

    this.handlerDiscover = bleManagerEmitter.addListener('BleManagerDiscoverPeripheral', this.handleDiscoverPeripheral);
    this.handlerStop = bleManagerEmitter.addListener('BleManagerStopScan', this.handleStopScan);
}
@前端进阶之旅: 代码已经复制到剪贴板

这里做了四件事。enableBluetooth 会弹一个系统级的请求,让用户把蓝牙开关打开,这个只在 Android 上有效,iOS 不允许应用直接开关蓝牙,只能引导用户自己去设置里开。start 是初始化 BleManager 模块本身,showAlert: false 的意思是蓝牙没开的时候不要弹那个默认的系统提示框,因为我们自己已经用 enableBluetooth 处理过了。

然后是两个事件监听。BleManagerDiscoverPeripheral 每扫到一个外围设备就触发一次,这是整套逻辑真正干活的地方。BleManagerStopScan 在一轮扫描结束时触发。

这块有个执行顺序的问题要注意。上面这段是把 enableBluetooth 和 start 并排写的,两个都是异步的,谁先完成不确定。更稳的写法是先 start,等它 resolve 之后再去 enableBluetooth 和挂监听,因为 start 没完成之前原生模块还没准备好,这时候注册的监听有可能收不到事件。

// 更稳的初始化顺序:先 start,再处理蓝牙开关和监听
initDevInfo = async () => {
  await BleManager.start({ showAlert: false })
  if (Platform.OS === 'android') {
    try {
      await BleManager.enableBluetooth()
    } catch (e) {
      console.log('用户拒绝开启蓝牙')
    }
  }
  await this.onCheckLocation()
  this.handlerDiscover = bleManagerEmitter.addListener(
    'BleManagerDiscoverPeripheral',
    this.handleDiscoverPeripheral
  )
  this.handlerStop = bleManagerEmitter.addListener(
    'BleManagerStopScan',
    this.handleStopScan
  )
}
@前端进阶之旅: 代码已经复制到剪贴板

# 三、Android 上不给定位权限就扫不到设备

这个是我当时排查了整整一下午的问题。代码一行没错,iOS 上一切正常,Android 上 scan 调用成功、BleManagerStopScan 也按时触发,就是一个 BleManagerDiscoverPeripheral 都收不到。

原因在 Android 系统这边。从 Android 6.0 开始,BLE 扫描被归类成了「可以推断用户位置」的能力,因为周围有哪些蓝牙信标本身就能定位。所以系统要求应用必须持有位置权限才允许扫描,没有权限的时候扫描接口照常返回成功,只是结果列表永远是空的。不报错,不提示,就是空。

这个设计我一开始也没想到,找了半天以为是设备广播有问题。

所以要有这么一段权限检查。

 //检查是否获得定位权限
	 onCheckLocation =()=>{
        if(Platform.OS === 'ios'){
            return false;
        }
        const granted =PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION)

        granted.then((data)=>{
            if(!data){
                this.requestLocationPermission()
            }
        }).catch((err)=>{
            console.log('err---------',err.toString())
        })
    }
@前端进阶之旅: 代码已经复制到剪贴板

PermissionsAndroid.check 只查不弹框,返回一个 Promise。没有权限就走到 requestLocationPermission 去真正申请。

    //申请地址权限 有些安卓设备比如华为,需要开启定位才可以扫描到蓝牙设备
    async requestLocationPermission() {
        try {
            const granted = await PermissionsAndroid.request(
                PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
                {
                    //第一次请求拒绝后提示用户你为什么要这个权限
                    'title': '是否允许地址查询权限',
                    'message': '此权限会造成系统异常,请允许',
                    buttonNeutral: '等会再问我',
                    buttonNegative: '不行',
                    buttonPositive: '好的',
                }
            )

            if (granted === PermissionsAndroid.RESULTS.GRANTED) {
                showMsg("你已获取了定位权限")
            } else {
                showMsg("获取定位权限失败,会造成系统异常")
            }
        } catch (err) {
            showMsg(err.toString())
        }
}
@前端进阶之旅: 代码已经复制到剪贴板

PermissionsAndroid.request 的第二个参数是弹框文案。这段文案值得多花点心思,因为用户看到「运动 App 要定位权限」的第一反应就是拒绝。原文这个「此权限会造成系统异常,请允许」的写法其实不太好,容易被当成恐吓,写成「用于搜索附近的心率设备,不会记录你的位置」这类说明性文案,通过率会高一些。

还有个坑是光有权限不够。部分机型(华为、小米那批)还要求系统的定位开关本身处于打开状态,权限给了但定位总开关关着,照样扫不到。这个用 PermissionsAndroid 检测不出来,得靠原生模块去读系统的 location 服务状态,或者干脆在扫不到设备时给用户一句提示,引导他去下拉菜单打开定位。

# 四、扫描的四个参数

扫描是这样发起的。

	/**
     *开始扫描蓝牙
     */
    startScan = ()=>{
		if(bleScanTimer) clearInterval(bleScanTimer);

		bleScanTimer = setInterval(()=>{
			//扫描可用的外围设备
			BleManager.scan(["180d"], 8, false, { "scanMode": 2, "numberOfMatches": 3 }).then((results) => {
				console.log('开始扫描')
				if(!this.state.scanning) {
					this.setState({ scanning: true });
				}
			});
		}, timerHeartInterval)
	}
	stopScan = ()=>{
		if(bleScanTimer) clearInterval(bleScanTimer);
		this.setState({ scanning: false });
	}
@前端进阶之旅: 代码已经复制到剪贴板

BleManager.scan 这四个参数各管一摊,值得逐个说。

第一个 ["180d"] 是服务 UUID 过滤器。180d 是蓝牙标准里心率服务的短 UUID,只有广播里声明了这个服务的设备才会被上报。加过滤能省电,也能避免回调被周围一堆蓝牙耳机、手环刷屏。如果传空数组就是不过滤,什么都上报。

第二个 8 是本轮扫描持续的秒数。到点自动停,触发 BleManagerStopScan。

fe
  • 一、先选路线,广播还是连接
  • 二、初始化和事件监听
  • 三、Android 上不给定位权限就扫不到设备
  • 四、扫描的四个参数
  • 五、从广播包里解析心率
  • 六、生命周期和资源回收
  • 七、这几年蓝牙权限变了不少
  • 总结
  • 参考

← React Native相机扫码实战从权限配置到扫描线动画本地构建部署easy-mock服务完整流程 →