
IoT之低功耗蓝牙
低功耗蓝牙简介
低功耗蓝牙(Bluetooth Low Energy,简称 BLE)是一种短距离无线通信技术,它与经典蓝牙的主要区别在于,低功耗蓝牙传输距离短,传输速度快,每次传输的数据量小,功耗低,适合用于电池供电的设备,而经典蓝牙适合用于传输文件,功耗较大。
蓝牙和 Wi-Fi 调用在本质上都是一样的,本文基于React Native来示例说明。
低功耗蓝牙的使用流程是:
- 扫描设备列表
- 连接设备,这一步获得设备 ID
- 扫描设备服务,这一步获得服务 ID
- 扫描设备特征,这一步获得特征 ID
- 读写设备特征,不同的特征扮演不同的角色,有些负责读数据,有些负责写数据,有些负责做广播通知
- 断开连接
简单来说,蓝牙通信就是给某个特征收发消息,可类比为给某个小区的某一栋的某个人沟通。
这其中最主要的是要知道哪个特征负责什么任务,这一步往往需要跟硬件工程师沟通,拟定协议。
代码实现
请求权限
本质上安卓和 iOS 都需要权限。
iOS
在Info.plist中添加NSBluetoothAlwaysUsageDescription字段,这是你向用户请求蓝牙权限的说明。
iOS 无需手动请求权限,权限弹窗会在你使用蓝牙 API 的时候自动弹出。
Android
在AndroidManifest.xml文件中添加以下权限内容:
1 | <!-- Android >= 12 --> |
安卓需要手动请求权限,利用PermissionAndroid请求对应的权限:
1 | const result = await PermissionsAndroid.requestMultiple([ |
需要注意的是,蓝牙扫描权限和位置权限密不可分。
安装与配置
1 | npx expo install react-native-ble-plx |
app.json中添加 config plugin 配置,详见官方 README,以下是示例:
1 | { |
初始化
1 | import { BleManager } from "react-native-ble-plx"; |
某些情况下,用户可能没有打开蓝牙,你可能会有提醒用户打开蓝牙的需求。
可以利用manager.state()方法获取当前蓝牙状态,所有的功能方法,包括扫描,连接等,都必须在蓝牙状态为PoweredOn时才能调用。
对于安卓平台,可以使用manager.enable()方法手动打开蓝牙功能,对于 iOS 平台,你只能手动引导用户去打开蓝牙。
打开后,通过manager.onStateChange()方法监听蓝牙打开状态:
1 | const subscription = manager.onStateChange((state) => { |
扫描周边设备列表
使用manager.startDeviceScan()方法扫描周边的设备列表,该方法会在每次扫描到一个新设备后进行回调。并返回你一个Device对象。
1 | function scanAndConnect() { |
至此,已经拿到了自己需要的设备信息,我们可以连接设备,扫描设备服务和特征等。
需要注意的是,device.id在安卓上对应 MAC 地址,在 iOS 上对应 UUID。
连接设备
一定要先连接蓝牙,才能进行后续的读取、写入操作。拿到 Device 对象后,调用connect()方法即可连接设备。
1 | device |
至此,我们连接设备,并拿到了该设备上所有支持的服务和特征。特征信息保存在Service类里面,可以进行读写数据了!
消息读写
写入
通常来说,这一步需要跟硬件工程师协商,我们要知道每个特征具体负责什么任务,传递什么样的数据,这个数据协议往往要跟硬件工程师一起设计。
比如Shelly 电表文档中直接写出,向b0a7e40f-2b87-49db-801c-eb3686a24bdb这个特征写入1即可重置设备。
假设你真的无法跟硬件工程师沟通,你可以通过遍历service.characteristics(),拿到所有的特征列表,通过判断 UUID,以及可读写状态,来确定你需要的特征。
注意
低功耗蓝牙传输中,必须传递的是二进制数组,不能直接传递字符串。但是react-native-ble-plx提供的方法,只能传递 Base64,所以应当先拿到二进制数组,再转化成 Base64 字符串。
低功耗蓝牙传输中,每次传输的数据长度有限制。ATT MTU(Attribute Protocol Maximum Transmission Unit)是 BLE 通信中一次可以传输的最大字节数。默认情况下它是 23 字节。但是协议头会占据 3 字节,所以默认情况下每次传输的数据长度不能超过 20 字节。
好在 BLE 可以通过 MTU 协商,来增大这个值,在大多数现代设备上,MTU 可以协商到 247 字节甚至更高!
但是总的来说,MTU 协商是不可靠的,不同厂商的设备支持的 MTU 值可能不同,基于 MTU 协商的数据传输不能保证每次都能传输成功,所以我们往往需要对数据进行切片,多次传输。
具体还是要看设备,有些设备不需要协商,本身就支持很大的 MTU 长度。
大小端序
大小端序(Endianness)是指在多字节数据在内存中的存储顺序。
通常来说,我们在处理多字节数据时,需要知道数据在内存中的存储顺序,才能正确的解析数据。
比如,一个 32 位整数0x12345678,在内存中的存储顺序可能是:
- 大端序(Big Endian):
0x12 0x34 0x56 0x78 - 小端序(Little Endian):
0x78 0x56 0x34 0x12
以伪代码日常口语举例,1234(一千二百三十四),在内存中的存储顺序可能是:
- 大端序(Big Endian):
0x01 0x02 0x03 0x04 - 小端序(Little Endian):
0x04 0x03 0x02 0x01
为什么单独拿出来说明,因为计算机中的存储顺序往往是小端序(Little Endian),那么硬件同学往往也会使用小端序来读写数据,这中间涉及到数据的字节序转换问题,以及软硬件同学的理解偏差问题,不可不察。
以常规的配网流程为例,用户拿到设备后,首先要跟设备进行配对,然后设置该设备的基本信息,比如网络连接等。
1 | import { Buffer } from "buffer"; |
考虑到 BLE 的传输长度限制,实际业务中往往需要先下发字节长度,再下发实际的数据,具体要看跟硬件同学怎么定。
比如,假设你要发送[0x00]为消息体,那么你可以把消息总长度拼接在消息体之前,比如[0x01, 0x00, 0x00],前两个字节是小端序的整数0x0001,表示消息体长度为 1 字节,最后的0x00是消息体的内容。
关于有响应写入和无响应写入
writeCharacteristicWithResponseForDevice和writeCharacteristicWithoutResponseForDevice的区别:WithResponse:需要等待设备返回确认,才能继续写入下一个数据。可能耗时较长。WithoutResponse:不需要等待设备返回确认,就可以继续写入下一个数据。可能会丢数据。
两者的传输模式有区别,需要跟硬件同学协商,如果某特征只支持WithoutResponse,那么就不能使用WithResponse方法去写入,会写入失败。
关于蓝牙传输的数据本质
问:蓝牙传输过程中,二进制,十进制,十六进制,这些词到处出现,那么这些东西到底是什么?蓝牙传输的到底是什么进制?
答:蓝牙传输的是二进制数据流,本质上就是0-255之间的整数,是蓝牙底层的传输处理方式,通常来说我们需要接触到的是十进制或者十六进制。
简单来说,同样的数据,展示方式可能不同,比如:
- 二进制:
10101010 - 十进制:
170 - 十六进制:
0xAA
app 客户端在处理的时候可能接触的是十进制,下发的时候传递的可能是AA,但是最终到了蓝牙硬件层,都是通过二进制数据流来传输的。
假设你得到一组十进制数据(Hello 的 ASCII 码)[72, 101, 108, 108, 111],如果你不考虑加密等问题,你可以直接用String.fromCharCode()方法拿到字符串,或者也可以利用Buffer.from()方法进行转码。
监听
客户端有时需要实时监听来自设备的回信。例如,在 Wi-Fi mesh 组网之后,设备通过蓝牙告知手机,mesh 中存在多少台设备,或者配网之后,上报给手机,通知连接 Wi-Fi 成功。
1 | device.monitorCharacteristicForService( |
手动读取
1 | const result = await device.readCharacteristicForService( |
蓝牙加密
在实际业务中,或者出于合规要求,我们往往需要对蓝牙传输的数据内容进行加密。但需要记住,蓝牙传输的是二进制流的本质是不变的,加密只是对文字内容的加密。
其中,认证标签的长度需要前后端进行约定。
本文基于对称加密算法 AES 128 GCM 举例说明。
加密流程如下:
- 加密端生成随机的 12 字节的初始化向量(IV/Nonce)
- 加密端确定加密密钥,通常为 16 字节
- 加密端对数据进行加密,得到加密后的二进制流
- 加密端将加密后的密文,和 Key 以及 IV 一起发送给解密端
- 解密端:拆分出 Key、IV、密文,使用与加密端同样的 Key 和 IV,进行解密
传输过程中,将 IV,密文,Key 的字节数组拼接在一起,一起发送给对方。
加解密代码举例:
1 | encryptAES128GCM( |
解密
1 | decryptContent(byteArray: number[], key: string): string { |
注意事项
平台特性
- 在安卓上,我们可以手动控制蓝牙的开启状态,而在iOS上,你只能引导用户在控制中心中开启蓝牙。注意是控制中心,而不是系统设置。
- iOS系统没有提供协商MTU的API,它是自动的,而安卓需要手动需要requestMTU协商。
- 接上条,iOS虽然无法手动协商MTU,但是它似乎可以获取当系统给你协商好的MTU值,只是可能需要等待发送消息之后,才能去获取。
- 标题: IoT之低功耗蓝牙
- 作者: Aron
- 创建于 : 2025-06-29 20:26:55
- 更新于 : 2025-10-28 19:06:59
- 链接: https://likeso.github.io/2025/06/29/iot-ble/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。