项目中使用了一个社区不维护的蓝牙库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';
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 连接流程图
%20%E8%93%9D%E7%89%99%20BLE%20%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ble_connection_flow.svg)
3.2 创建 GATT 客户端
1 2
| 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) => { console.info(`连接状态变更: ${state.state}`); if (state.state === 2) { } else if (state.state === 0) { } });
|
3.4 发起连接
重要: 在调用 connect() 前,建议显式停止蓝牙扫描,以提高连接稳定性。
4. 发现服务 (Service Discovery)
连接成功后,需要获取设备支持的服务列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| try { 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, descriptors: [] };
const writeType = ble.GattWriteType.WRITE;
try { await gattClient.writeCharacteristicValue(characteristicObj, writeType); console.info('写入成功'); } catch (e) { console.error(`写入失败: ${e.message}`); }
|
6. 接收数据 (Receiving Data)
6.1 通知流程图
%20%E8%93%9D%E7%89%99%20BLE%20%E5%BC%80%E5%8F%91%E6%8C%87%E5%8D%97/ble_notification_flow.svg)
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)
要接收设备主动推送的数据,需要完成三个步骤:
- 开启本地通知处理 (
setCharacteristicChangeNotification)
- 写入 CCCD 描述符 (告知设备开启推送)
- 监听数据变化事件 (
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 = '...';
const notifyCharacteristic: ble.BLECharacteristic = { serviceUuid: serviceUuid, characteristicUuid: characteristicUuid, characteristicValue: new ArrayBuffer(0), descriptors: [] };
await gattClient.setCharacteristicChangeNotification(notifyCharacteristic, true);
const cccdUuid = '00002902-0000-1000-8000-00805f9b34fb'; const cccdValue = new Uint8Array([1, 0]).buffer;
const cccdObj: ble.BLEDescriptor = { serviceUuid: serviceUuid, characteristicUuid: characteristicUuid, descriptorUuid: cccdUuid, descriptorValue: cccdValue }; await gattClient.writeDescriptorValue(cccdObj);
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
|
let filters: Array<ble.ScanFilter> | null = null; if (serviceUuids.length > 0) { filters = serviceUuids.map(uuid => ({ serviceUuid: uuid })); }
const scanOptions: ble.ScanOptions = { interval: 500, dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER, matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE };
ble.startBLEScan(filters, scanOptions);
ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => { data.forEach(result => { }); });
|
9. 数据转换与编码
在鸿蒙 ArkTS 与原生蓝牙接口交互时,经常需要在 ArrayBuffer、Uint8Array 和 Base64 之间转换。
- 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';
function toBase64(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); const helper = new util.Base64Helper(); return helper.encodeToStringSync(bytes); }
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. 常见问题与避坑指南
- 连接前停止扫描: 在调用
connect() 之前,务必确保已经停止了 BLE 扫描,否则连接请求可能会被忽略或超时。
- 重复创建实例: 避免对同一个设备 ID 重复调用
createGattClientDevice,应维护一个 Map 缓存已创建的 GattClientDevice 实例。
- 服务发现时序: 连接成功 (Connected) 后,立即调用
getServices() 可能会偶尔报错 (错误码 300),这是底层未完全就绪,建议在 Dart/JS 层做适当的重试或忽略非致命错误。
- MTU 协商: 鸿蒙支持
gattClient.setBLEMtuSize(mtu)。
参考资料