一、背景说明

低功耗蓝牙(BLE)是可穿戴设备、传感器、耳机配件类应用的基础连接能力。近期星闪耳返等近距连接功能在 HarmonyOS 7 上陆续上线,近距通信生态持续升温,而 BLE 仍是应用侧最通用的设备发现与数据传输通道。本文基于 Connectivity Kit 的 @ohos.bluetooth.ble 模块,完整实现"扫描周边 BLE 设备 → 发起 GATT 连接 → 读写特征值"全流程,代码可在 API 12 及以上(含 26.0.0)工程中运行。

二、技术原理

BLE 采用 GATT(通用属性协议)组织数据:设备端提供服务(Service),服务下包含特征值(Characteristic),数据读写在特征值上进行。应用可作为两种角色:

  1. GATT 客户端:主动扫描广播、发起连接、读写对端特征值——手机 App 通常扮演此角色;
  2. GATT 服务端:向外发送广播并在被连接后提供数据——外设扮演此角色。

扫描与连接的数据流为:申请 ohos.permission.ACCESS_BLUETOOTH(user_grant 用户授权权限)→ ble.startBLEScan 发起扫描(或 API 15+ 的 createBleScanner 多路扫描)→ on('BLEDeviceFind') 回调拿到设备地址 → ble.createGattClientDevice 创建客户端 → connect() 建链 → getServices() 发现服务 → 读写特征值 / 订阅 Notify。连接成功后需把 MTU 调大(setBLEMtuSize),单次写入的数据量受 MTU-3 字节限制。

三、实现步骤

module.json5 中声明权限(ACCESS_BLUETOOTH 为用户授权权限,需运行时弹窗申请):

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.ACCESS_BLUETOOTH", "reason": "$string:bt_reason" }
    ]
  }
}

新建 src/main/ets/ble/BleManager.ets(完整代码,扫描 + 连接 + 读写特征值封装):

import { ble } from '@kit.ConnectivityKit';
import { permissions } from '@kit.AbilityKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG: string = 'BleManager';
// 自定义服务与特征值 UUID(与对端设备约定一致)
const SERVICE_UUID: string = '00001888-0000-1000-8000-00805f9b34fb';
const CHAR_UUID: string = '00002a37-0000-1000-8000-00805f9b34fb';

export class BleManager {
  private client: ble.GattClientDevice | null = null;

  // 运行时申请用户授权权限
  async requestBlePermission(context: common.UIAbilityContext): Promise<boolean> {
    try {
      const result = await permissions.requestPermissionsFromUser(context, {
        permissions: ['ohos.permission.ACCESS_BLUETOOTH'],
        requestPermissions: ['ohos.permission.ACCESS_BLUETOOTH']
      });
      return result.authResults.every(r => r === 0);
    } catch (err) {
      const e: BusinessError = err as BusinessError;
      hilog.error(TAG, 'requestPermission failed: %{public}d %{public}s', e.code, e.message);
      return false;
    }
  }

  // 扫描指定服务的 BLE 设备
  startScan(onDevice: (deviceId: string, name: string) => void): void {
    try {
      ble.on('BLEDeviceFind', (results: Array<ble.ScanResult>) => {
        for (const r of results) {
          const name = r.deviceName ?? '';
          hilog.info(TAG, 'found: %{public}s %{public}s', r.deviceId, name);
          onDevice(r.deviceId, name);
        }
      });
      const filter: ble.ScanFilter = { serviceUuid: SERVICE_UUID };
      ble.startBLEScan([filter], { dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER });
    } catch (err) {
      const e: BusinessError = err as BusinessError;
      hilog.error(TAG, 'startScan failed: %{public}d %{public}s', e.code, e.message);
    }
  }

  stopScan(): void {
    try {
      ble.off('BLEDeviceFind');
      ble.stopBLEScan();
    } catch (err) {
      const e: BusinessError = err as BusinessError;
      hilog.error(TAG, 'stopScan failed: %{public}d', e.code);
    }
  }

  // 连接目标设备并发现服务
  connect(deviceId: string): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        this.client = ble.createGattClientDevice(deviceId);
        this.client.on('BLEConnectionStateChange', (state: ble.BLEConnectionChangeParam) => {
          hilog.info(TAG, 'state: %{public}d', state.state);  // 0断开 2已连接
          if (state.state === 0) { this.client = null; }
        });
        this.client.connect();
        setTimeout(() => {
          this.client?.getServices().then((services: Array<ble.GattService>) => {
            hilog.info(TAG, 'services: %{public}d', services.length);
            resolve();
          }).catch((err: BusinessError) => reject(err));
        }, 1500);   // 等待建链完成后再发现服务
      } catch (err) {
        reject(err as BusinessError);
      }
    });
  }

  // 写特征值(单次写入长度 ≤ MTU-3 字节)
  async writeData(data: Uint8Array): Promise<void> {
    if (!this.client) { throw new Error('not connected'); }
    await this.client.setBLEMtuSize(247);
    const char: ble.BLECharacteristic = {
      serviceUuid: SERVICE_UUID,
      characteristicUuid: CHAR_UUID,
      descriptorUuid: '00002902-0000-1000-8000-00805f9b34fb',
      value: data.buffer
    };
    await this.client.writeCharacteristicValue(char, ble.GattWriteType.WRITE);
  }

  // 订阅特征值 Notify 接收对端数据
  subscribeNotify(onData: (v: Uint8Array) => void): void {
    if (!this.client) { return; }
    const char: ble.BLECharacteristic = {
      serviceUuid: SERVICE_UUID,
      characteristicUuid: CHAR_UUID,
      descriptorUuid: '00002902-0000-1000-8000-00805f9b34fb',
      value: new ArrayBuffer(0)
    };
    this.client.setCharacteristicChangeNotification(char, true);
    this.client.on('CharacteristicChange', (r: ble.CharacteristicChangeResponse) => {
      onData(new Uint8Array(r.characteristic.value));
    });
  }

  disconnect(): void {
    try { this.client?.disconnect(); this.client = null; } catch (err) { }
  }
}
四、应用场景示例

在 Index.ets 中串联"授权 → 扫描 → 连接 → 写入"流程:

import { BleManager } from '../ble/BleManager';

@Entry
@Component
struct Index {
  private ble: BleManager = new BleManager();
  @State log: string = '就绪';

  build() {
    Column({ space: 12 }) {
      Text(this.log).fontSize(16)
      Button('扫描并连接').onClick(async () => {
        const ctx = getContext(this) as common.UIAbilityContext;
        if (!await this.ble.requestBlePermission(ctx)) {
          this.log = '蓝牙权限被拒绝'; return;
        }
        this.ble.startScan((id, name) => {
          this.log = `发现设备 ${name} ${id}`;
          this.ble.stopScan();
          this.ble.connect(id)
            .then(async () => {
              this.log = '已连接,写入数据';
              await this.ble.writeData(new Uint8Array([0x01, 0x02]));
            })
            .catch((e: BusinessError) => { this.log = `连接失败 ${e.code}`; });
        });
      })
      Button('断开').onClick(() => { this.ble.disconnect(); this.log = '已断开'; })
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center)
  }
}
五、技术要点
  1. 权限模型:ACCESS_BLUETOOTH 是 user_grant 权限,仅在 module.json5 声明不够,必须运行时调用 requestPermissionsFromUser,拒绝后所有扫描/连接调用抛 201;
  2. 扫描互斥:startBLEScan 是单路扫描,重复调用前必须先 stopBLEScan,否则抛 2900001;需要并行多路扫描时用 API 15+ 的 ble.createBleScanner 创建独立实例;
  3. 蓝牙未开:调用前可检查蓝牙状态,未开启抛 2900003,应引导用户去设置页开启;
  4. 建链时序:connect() 后立即 getServices() 可能拿不到服务,需等待连接状态回调或适当延时后再发现服务;
  5. MTU 限制:默认 MTU 较小,写入大数据前先 setBLEMtuSize,且单次不超过 MTU-3 字节,超长数据需分包发送;
  6. 操作串行化:GATT 单链路上的读写与订阅操作必须等上一个 Promise 完成再发起下一个,并发调用抛 2900011(operation busy)。
六、扩展建议

若需要同时维持扫描与连接(如后台持续发现新配件设备),改用多路扫描:

const scanner = ble.createBleScanner();
scanner.startScan([{ serviceUuid: SERVICE_UUID }], { dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER });
scanner.on('BLEDeviceFind', (r: Array<ble.ScanResult>) => { /* 处理 */ });

26.0.0 起 createGattClientDevice 还支持传入 GattSetting 配置连接参数(超时、传输类型等),自动重连场景可在 connect 时设置 autoConnect: true,设备回到广播范围后系统会自动建链。

结语

本文用约 120 行 ArkTS 完成 BLE 从权限申请、设备扫描到 GATT 连接、特征值读写与 Notify 订阅的完整闭环,核心是 startBLEScan → createGattClientDevice → connect 三步链路加 MTU 与操作时序控制。掌握这条链路即可支撑耳机配对、传感器采集等绝大多数近距场景的开发。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐