大家好,我是simple。我的理想是利用科技手段来解决生活中遇到的各种问题。
1. 概念
1.1 蓝牙种类
蓝牙是短距离无线通信技术,核心是让设备无线互联。它分传统蓝牙和低功耗蓝牙(BLE)两大分支。
传统蓝牙适合传音频、文件等大数据,功耗较高;BLE 则主打超低功耗,续航能达数月甚至数年,适合小数据传输,是蓝牙设备的 "常客"。
在开发之前我们需要分清楚自己需要开发的设备是哪个类型(在这里我们主要是讲低功耗设备的通讯)。
1.2 中心设备和外围设备
在低功耗蓝牙(BLE)通信中,中心设备和外围设备是两种核心角色,分工明确:
中心设备是 主动方 ,负责扫描、发起连接并获取数据,比如鸿蒙手机、平板等。它像 指挥官,主动搜索周围的外围设备,建立连接后读取或控制数据(例如手机连接智能手环时,手机就是中心设备)。
外围设备是 被动方 ,通过广播信号 告知 自身存在,等待被连接,比如智能手环、温湿度传感器。它就像 喇叭,不断向外播报少量数据(如步数、温度),被中心设备连接后才进一步交互。
1.3 广播与扫描
-
广播(Advertising) :外围设备(如传感器)通过广播包向外发送自身信息(如设备名称、支持的服务 UUID),让中心设备能发现它。广播每次发出都会有时间间隔,因而在扫描的时候会时不时出现一个设备。
-
扫描(Scanning) :中心设备(如手机)主动扫描周围的广播包,筛选出目标设备后发起连接。
1.4 客户端和服务端
在低功耗蓝牙(BLE)通信中,服务端(Server) 和客户端(Client) 是从 "数据交互模式" 定义的角色,与 "中心设备 / 外围设备"(连接发起模式)是两个不同维度的概念。
- 服务端(Server) :是 "数据提供者",负责定义通信的数据结构(如 "温度服务""步数服务"),并对外提供数据读写接口。可以理解为 "存有数据的仓库",等待被访问。
- 客户端(Client) :是 "数据请求者",通过服务端提供的接口读取数据或发送指令。可以理解为 "访问仓库的人",主动发起数据交互。
中心设备可以充当服务端或客户端,外围设备也可以充当服务端或客户端。
1.5 层级关系:设备 → 服务 → 特征
当客户端(如鸿蒙手机应用)需要和 BLE 设备交互时,流程通常是:
- 连接设备后,先 "发现服务"(获取设备包含哪些服务,如找到
0x180F
电池服务); - 在目标服务中 "发现特征"(如找到电池服务下的
0x2A19
电池电量特征); - 检查特征的 Properties(如是否支持
READ
); - 根据权限操作:支持
READ
就调用读 API 获取数据(如读取电池电量),支持NOTIFY
就注册通知(如实时接收心率变化)。
2. 流程
了解了上述概念之后,那我们写代码实现就简单多了。
这里我们主要举例子鸿蒙手机为中心设备建立客户端,与外围设备建立联系的案例。
- 开启蓝牙
- 搜索周边蓝牙设备
- 连接蓝牙设备
- 建立client客户端并指定服务和指定特征
- 读取或写入数据
3. 代码实现
- 开启蓝牙
ts
import { access, ble } from '@kit.ConnectivityKit';
// 当前蓝牙状态:开启、关闭、开启中、关闭中
get state() {
return access.getState()
}
// 开启蓝牙开启关闭监听
onStateChange(callback: Callback<access.BluetoothState>) {
access.on('stateChange', callback)
}
offStateChange(callback?: Callback<access.BluetoothState>) {
access.off('stateChange', callback)
}
- 搜索周边蓝牙设备
ts
// 扫描设备
onDeviceFind(callback: (list: Array<ble.ScanResult>) => void) {
ble.on("BLEDeviceFind", this.findDeviceCallback)
}
offDeviceFind() {
ble.off("BLEDeviceFind")
}
// 停止扫描
stopScan() {
ble.stopBLEScan()
}
// 开启扫描
async scan(callback: (data: Array<ble.ScanResult>) => void) {
this.onDeviceFind(callback)
// tips: 这里扫描容易报错,是因为不能开启扫描,所以在开启新的扫描之前必须关闭上一个扫描
this.stopScan()
let scanOptions: ble.ScanOptions = {
interval: 500,
dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER,
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
}
ble.startBLEScan([{}], scanOptions);
}
- 连接蓝牙设备
ts
async connect(deviceId: string) {
if (this.currentDeviceId === deviceId) {
return
}
this.client = ble.createGattClientDevice(deviceId)
this.client.connect()
}
- 建立client客户端并指定服务和指定特征
ts
async getCharacter(characterId: string): Promise<ble.BLECharacteristic | void> {
if (this.client) {
const serves = await this.client.getServices()
const service = serves.find(item => item.serviceUuid.startsWith(MoonLiteManager.SERVICE_ID))
if (service) {
return service.characteristics.find(item => item.characteristicUuid.startsWith(characterId))
}
}
return null
}
- 读取或写入数据
ts
// 写入数据
async writeMessage(characteristicValue: string) {
const writeCharacter = await this.getCharacter(MoonLiteManager.WRITE_CHARACTERISTIC_ID)
if (writeCharacter && this.client) {
try {
this.client.writeCharacteristicValue({
serviceUuid: writeCharacter.serviceUuid,
characteristicValue: hexStringToBytes(characteristicValue),
characteristicUuid: writeCharacter.characteristicUuid,
descriptors: writeCharacter.descriptors
}, ble.GattWriteType.WRITE)
} catch (e) {
e
}
}
}
// 监听写入消息是否成功的回调
// 回调数据由服务端返回
async onMessage() {
const readCharacter = await this.getCharacter(MoonLiteManager.READ_CHARACTERISTIC_ID)
if (!readCharacter || !this.client) {
return
}
await this.client.setCharacteristicChangeNotification(readCharacter, true);
this.client.on('BLECharacteristicChange', (data) => {
const arrayBuffer = new Uint8Array(data.characteristicValue)
const key = formatArrayBuffer(arrayBuffer).toUpperCase()
if (key.startsWith(MoonLiteManager.PRESSURE_COMMAND)) {
this.pressureManager.emitPressure(arrayBuffer)
}
});
}