鸿蒙 (HarmonyOS) 蓝牙 BLE 开发指南

项目中使用了一个社区不维护的蓝牙库flutter_ble_lib,从flutter1.22.6到目前flutter3.34.5一直放到私仓中维护;因为一直用串口模拟数据没有真实的设备,所以也就没迁移到flutter_blue_plus;

纯血鸿蒙版本的公司项目已经上架应用市场很久了,项目中蓝牙相关功能一直缺失,所以抽时间适配了蓝牙。

因为在适配中遇到很多问题(DevEco Studio中的AI回复中有很多错误),所以特在此处记录,便于后期查阅;

1
2
3
4
5
鸿蒙版flutter3.22.0 
https://gitcode.com/openharmony-tpc/flutter_flutter

相关插件
https://gitcode.com/openharmony-tpc/flutter_packages/blob/master/README.md

本文档整理了在 HarmonyOS Next (API 11+) 上进行低功耗蓝牙 (BLE) 开发的核心 API 及流程。

1. 权限配置

module.json5 中声明必要的权限:

1
2
3
4
5
6
7
8
9
"requestPermissions": [
{
"name": "ohos.permission.ACCESS_BLUETOOTH",
"reason": "$string:bluetooth_reason",
"usedScene": {
"when": "inuse"
}
}
]

注意1: ohos.permission.MANAGE_BLUETOOTH 是特权权限,普通应用不应申请,否则会导致安装失败。扫描蓝牙通常需要位置权限。

1.1 运行时权限申请代码

在插件的中,实现了运行时的权限申请逻辑。虽然 ACCESS_BLUETOOTH 通常是系统授权,但插件中仍进行了显式申请:

OpenHarmony封装的permission_handler有问题,没时间看它的代码逻辑,所以自己在当前插件中封装了下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { abilityAccessCtrl, common } from '@kit.AbilityKit';

// 上下文通常由 UIAbility 提供
async function requestPermissions(context: common.UIAbilityContext) {
try {
if (context) {
let atManager = abilityAccessCtrl.createAtManager();
// 申请权限
let data = await atManager.requestPermissionsFromUser(context, [
"ohos.permission.ACCESS_BLUETOOTH"
]);
console.info('权限申请结果:' + JSON.stringify(data));
}
} catch (err) {
console.error(`申请权限失败: ${JSON.stringify(err)}`);
}
}

2. 导入模块

1
2
import ble from '@ohos.bluetooth.ble';
import { BusinessError } from '@ohos.base';

3. 连接设备 (Binding/Connecting)

连接设备主要分为三步:创建客户端实例、设置状态监听、发起连接。

3.1 连接流程图

ble_connection_flow.png

3.2 创建 GATT 客户端

1
2
// remoteId 为扫描到的设备 MAC 地址
const gattClient = ble.createGattClientDevice(remoteId);

3.3 监听连接状态

必须在调用 connect() 之前设置监听器,否则可能错过连接成功的状态回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gattClient.on('BLEConnectionStateChange', (state: ble.BLEConnectionChangeState) => {
// state.state 枚举值:
// 0: DISCONNECTED
// 1: CONNECTING
// 2: CONNECTED
// 3: DISCONNECTING
console.info(`连接状态变更: ${state.state}`);

if (state.state === 2) {
// 连接成功
} else if (state.state === 0) {
// 连接断开
// 建议清理资源
// gattClient.close();
}
});

3.4 发起连接

重要: 在调用 connect() 前,建议显式停止蓝牙扫描,以提高连接稳定性。

1
2
3
// 发起连接
// 注意:鸿蒙 connect() 目前没有超时参数,建议业务层自行实现超时逻辑
gattClient.connect();

4. 发现服务 (Service Discovery)

连接成功后,需要获取设备支持的服务列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
// getServices 会触发服务发现或返回缓存的服务列表
const services = await gattClient.getServices();

services.forEach(service => {
console.info(`发现服务: ${service.serviceUuid}`);
// 遍历特征值
service.characteristics.forEach(characteristic => {
console.info(` 特征值: ${characteristic.characteristicUuid}`);
});
});
} catch (e) {
console.error(`发现服务失败: ${e.message}`);
}

5. 发送数据 (Writing Data)

向设备的特征值写入数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 构造特征值对象
const characteristicObj: ble.BLECharacteristic = {
serviceUuid: '0000xxxx-0000-1000-8000-00805f9b34fb',
characteristicUuid: '0000yyyy-0000-1000-8000-00805f9b34fb',
characteristicValue: myDataBuffer, // ArrayBuffer 类型
descriptors: []
};

// 写入类型:
// ble.GattWriteType.WRITE (带响应)
// ble.GattWriteType.WRITE_NO_RESPONSE (无响应)
const writeType = ble.GattWriteType.WRITE;

try {
await gattClient.writeCharacteristicValue(characteristicObj, writeType);
console.info('写入成功');
} catch (e) {
console.error(`写入失败: ${e.message}`);
}

6. 接收数据 (Receiving Data)

6.1 通知流程图

ble_notification_flow.png

6.2 读取特征值 (Read)

主动读取特征值的当前值。

1
2
3
4
5
6
7
8
9
10
11
12
13
const readReqObj: ble.BLECharacteristic = {
serviceUuid: '...',
characteristicUuid: '...',
characteristicValue: new ArrayBuffer(0),
descriptors: []
};

try {
const result = await gattClient.readCharacteristicValue(readReqObj);
console.info(`读取到的数据长度: ${result.characteristicValue.byteLength}`);
} catch (e) {
console.error(`读取失败: ${e.message}`);
}

6.3 订阅通知 (Notify/Indicate)

要接收设备主动推送的数据,需要完成三个步骤:

  1. 开启本地通知处理 (setCharacteristicChangeNotification)
  2. 写入 CCCD 描述符 (告知设备开启推送)
  3. 监听数据变化事件 (BLECharacteristicChange)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const serviceUuid = '...';
const characteristicUuid = '...';

// 1. 开启本地通知
const notifyCharacteristic: ble.BLECharacteristic = {
serviceUuid: serviceUuid,
characteristicUuid: characteristicUuid,
characteristicValue: new ArrayBuffer(0),
descriptors: []
};
// 第二个参数 true 表示开启
await gattClient.setCharacteristicChangeNotification(notifyCharacteristic, true);

// 2. 写入 CCCD (Client Characteristic Configuration Descriptor)
// UUID 固定为 00002902-0000-1000-8000-00805f9b34fb
// Value: [1, 0] 开启 Notify, [2, 0] 开启 Indicate
const cccdUuid = '00002902-0000-1000-8000-00805f9b34fb';
const cccdValue = new Uint8Array([1, 0]).buffer; // 示例:开启 Notify

const cccdObj: ble.BLEDescriptor = {
serviceUuid: serviceUuid,
characteristicUuid: characteristicUuid,
descriptorUuid: cccdUuid,
descriptorValue: cccdValue
};
await gattClient.writeDescriptorValue(cccdObj);

// 3. 监听数据回调
gattClient.on('BLECharacteristicChange', (characteristicChange: ble.BLECharacteristic) => {
if (characteristicChange.characteristicUuid === characteristicUuid) {
const data = new Uint8Array(characteristicChange.characteristicValue);
console.info(`收到数据: ${data}`);
}
});

7. 断开连接与资源释放

1
2
3
4
5
6
7
8
9
10
11
if (gattClient) {
try {
// 断开连接
gattClient.disconnect();

// 关闭客户端,释放资源 (重要)
gattClient.close();
} catch (e) {
console.error(`断开连接失败: ${e.message}`);
}
}

8. 扫描进阶配置 (Scanning)

鸿蒙的扫描接口需要注意参数的有效性,否则会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 过滤器 (ScanFilter)
// 若不使用过滤器,必须传递 null,而不能是空数组 [],否则报错
let filters: Array<ble.ScanFilter> | null = null;
if (serviceUuids.length > 0) {
filters = serviceUuids.map(uuid => ({ serviceUuid: uuid }));
}

// 2. 扫描选项 (ScanOptions)
const scanOptions: ble.ScanOptions = {
interval: 500, // 建议 > 0,设为 0 可能报 Invalid parameter
dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER, // -1: LowPower, 1: Balanced, 2: LowLatency
matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE
};

// 3. 开启扫描
ble.startBLEScan(filters, scanOptions);

// 4. 获取结果
ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => {
data.forEach(result => {
// 解析广播数据 (data.data 是 ArrayBuffer)
// 需手动解析 Manufacturer Data (0xFF) 和 Local Name (0x09)
});
});

9. 数据转换与编码

在鸿蒙 ArkTS 与原生蓝牙接口交互时,经常需要在 ArrayBufferUint8ArrayBase64 之间转换。

  • ArrayBuffer: 鸿蒙 BLE 接口的标准数据类型 (如 characteristicValue, descriptorValue).
  • Uint8Array: 便于操作字节数据的视图,常用于从 ArrayBuffer 创建。
  • Base64: 通常用于跨端传输 (如传给 Flutter/JS 层)。

常用转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { util } from '@kit.ArkTS';

// ArrayBuffer 转 Base64
function toBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
const helper = new util.Base64Helper();
return helper.encodeToStringSync(bytes);
}

// Base64 转 ArrayBuffer
function fromBase64(base64: string): ArrayBuffer {
const helper = new util.Base64Helper();
const bytes = helper.decodeSync(base64);
return bytes.buffer as ArrayBuffer;
}

10. 错误码速查表

在插件开发中常见的错误码及其含义:

错误码 (ErrorCode) 含义 可能原因 建议处理
300 Services Discovery Failed 连接建立后立即调用 getServices,底层尚未同步完成 忽略非致命错误,或延时重试
401 Characteristic Write Failed 写入数据格式错误、权限不足或设备断开 检查数据长度和连接状态
402 Characteristic Read Failed 特征值不可读或设备断开 检查特征值属性 (Readable)
403 Notify Change Failed setCharacteristicChangeNotification 失败或 CCCD 写入失败 确保按顺序开启通知
2900001 Service Operation Failed 蓝牙服务异常或未开启 检查蓝牙开关
2900003 Bluetooth Switch Off 蓝牙已关闭 提示用户开启蓝牙

11. 常见问题与避坑指南

  1. 连接前停止扫描: 在调用 connect() 之前,务必确保已经停止了 BLE 扫描,否则连接请求可能会被忽略或超时。
  2. 重复创建实例: 避免对同一个设备 ID 重复调用 createGattClientDevice,应维护一个 Map 缓存已创建的 GattClientDevice 实例。
  3. 服务发现时序: 连接成功 (Connected) 后,立即调用 getServices() 可能会偶尔报错 (错误码 300),这是底层未完全就绪,建议在 Dart/JS 层做适当的重试或忽略非致命错误。
  4. MTU 协商: 鸿蒙支持 gattClient.setBLEMtuSize(mtu)

参考资料