我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

老实说,蓝牙开发最容易把人整破防:文档一堆、设备千奇百怪、时序还挑剔。好消息是,在 HarmonyOS 上,BLE 这套路数已经越来越清晰了:权限先搞定 → 扫描&配对拿到目标 → 用 GATT 客户端稳定读写/订阅
  本文按你给的路线走:蓝牙权限配置 → 设备扫描与配对 → 数据传输;技术点聚焦 @ohos.bluetooth/* / @kit.ConnectivityKit 以及 GattClient 的正确打开方式。全程给到可跑的 ArkTS 代码骨架,外加若干“踩坑止血小贴士”。走起~🛠️

一、蓝牙权限配置(能不能“开口说话”的前提)

1)在 module.json5 里声明权限

HarmonyOS NEXT 下,开启/扫描/连接 BLE 的必需权限ohos.permission.ACCESS_BLUETOOTH。同时,建议在运行时做一次用户授权确认(系统会弹框)。(华为开发者)

// module.json5(节选)
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.ACCESS_BLUETOOTH",
        "reason": "$string:need_bluetooth",
        "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
      }
    ]
  }
}

小贴士:蓝牙“开关”可用 @ohos.bluetooth.access 模块的 enableBluetooth/enableBluetoothAsync 控制或监听,也需要 ACCESS_BLUETOOTH。(华为开发者)

2)导入模块与开关蓝牙

// BLE 基础导入(Stage 模型)
import { ble, access, constant } from '@kit.ConnectivityKit'
import { BusinessError } from '@kit.BasicServicesKit'

// 开启蓝牙(建议用 Async 版本,能拿到用户选择结果)
async function ensureBluetoothOn() {
  const state = access.getState()
  if (state === access.BluetoothState.STATE_OFF) {
    try {
      await access.enableBluetoothAsync()
    } catch (e) {
      const err = e as BusinessError
      console.error(`enableBluetoothAsync failed: ${err.code}`)
      throw e
    }
  }
}

二、设备扫描与配对(把“对的人”找出来)

1)BLE 扫描(service 过滤 + 监听回调)

  • API 进化点:新版本引入 BleScannercreateBleScanner()),支持更细的上报模式;老接口 startBLEScan 仍存在但逐步向下兼容/标注废弃,建议跟着新文档走。(华为开发者)
// 扫描:按服务 UUID 过滤,监听发现事件
type FoundDevice = { deviceId: string; name: string; rssi: number; adv?: Uint8Array }

export class Scanner {
  private scanner = ble.createBleScanner() // API 15+ 引入的扫描器形态 :contentReference[oaicite:3]{index=3}
  private found = new Map<string, FoundDevice>()

  onFound?: (list: FoundDevice[]) => void

  async start(serviceUuid?: string) {
    // 过滤条件(可选)
    const filters: ble.ScanFilter[] = serviceUuid ? [{ serviceUuids: [serviceUuid] }] : []
    await this.scanner.startScan(filters, { reportMode: ble.ScanReportMode.NORMAL })
    this.scanner.on('BLEDeviceFind', (report) => {
      // report.scanResult 是数组;这里做一次整理
      report.scanResult.forEach((it) => {
        const d: FoundDevice = {
          deviceId: it.deviceId,  // 注意:BLE 设备的 ID 可能是“随机地址” :contentReference[oaicite:4]{index=4}
          name: it.deviceName ?? '(unknown)',
          rssi: it.rssi,
          adv: it.payload
        }
        this.found.set(d.deviceId, d)
      })
      this.onFound?.(Array.from(this.found.values()).sort((a, b) => b.rssi - a.rssi))
    })
  }

  async stop() {
    this.scanner.off('BLEDeviceFind')
    await this.scanner.stopScan()
  }
}

实战建议

  • deviceId 可能是临时随机地址(隐私地址),不适合做“长久重连主键”。HarmonyOS 提供了持久化设备 ID 能力(access.addPersistentDeviceId/getPersistentDeviceIds)可做映射,业务上更稳。(华为开发者)
  • 扫描 UI 要做去抖与去重;RSSI 排序能让用户更快选到“近的”。

2)配对(Bonding)要不要?

  • BLE 场景很多时候不强制配对也能工作(明文服务/特征);但遇到受保护服务或设备要求“加密”时,需要走配对/绑定流程。
  • HarmonyOS 提供 @ohos.bluetooth.connection 的配对接口与事件订阅,用于传统蓝牙需要 Bonding 的 BLE场景。(码云)
import { connection } from '@kit.ConnectivityKit'

// 订阅配对状态变化(BOND_STATE_BONDED 即配对完成)
connection.on('bondStateChange', (data: connection.BondStateParam) => {
  console.info(`bond state: ${JSON.stringify(data)}`)
})

// 主动发起配对
async function pair(deviceId: string) {
  await connection.pairDevice(deviceId)  // 需要 ACCESS_BLUETOOTH 权限
}

小叮嘱:不同设备对配对 PIN/确认流程不一致,务必处理 pinRequired/确认 的用户交互(如系统弹框)。经典蓝牙(SPP 耳机/条码枪)走 socket/spp* 通道,和 BLE 的 GATT 是两条线;本文重点在 BLE。(华为开发者)


三、数据传输:GattClient 的“稳字诀”(连接→发现→读写→订阅)

攻略心法:把 GATT 当“状态机”来写。推荐流:
Idle → Connecting → ServicesDiscovered → MTUReady → RW/Notify,任何环节失败都要可重试 + 断线自愈

1)创建 GATT 客户端并连接

  • 客户端由 ble.createGattClientDevice(deviceId) 创建;
  • 连接结果、断线、MTU 变化、特征通知等事件要第一时间订阅。(华为开发者)
type UUID = string  // 形如 '0000180f-0000-1000-8000-00805f9b34fb'

export class GattClient {
  private gc!: ble.GattClientDevice
  private connected = false

  constructor(private deviceId: string) {
    this.gc = ble.createGattClientDevice(deviceId) // GATT 客户端实例
    // 连接状态
    this.gc.on('BLEConnectionStateChange', (state) => {
      console.info(`conn state: ${JSON.stringify(state)}`)
      this.connected = state.state === constant.ProfileConnectionState.STATE_CONNECTED
    })
    // 特征变化(notify/indication)
    this.gc.on('BLECharacteristicChange', (ch) => {
      console.info(`notify: ${ch.serviceUuid}/${ch.characteristicUuid} -> ${JSON.stringify(ch.characteristicValue)}`)
    })
    // MTU
    this.gc.on('BLEMtuChange', (mtu) => console.info(`mtu=${mtu}`))
  }

  async connect(timeoutMs = 10_000) {
    await this.gc.connect()              // 发起连接
    // 简易等待:业务里请用更健壮的等待/重试
    const t0 = Date.now()
    while (!this.connected) {
      await new Promise(r => setTimeout(r, 100))
      if (Date.now() - t0 > timeoutMs) throw new Error('connect timeout')
    }
  }

  async close() {
    try { await this.gc.disconnect() } finally { this.gc.close() }
  }
}

参考:官方 GATT 指南明确了“连接 → 服务查询 → 读写/通知”这一流程,与 Android GATT 十分类似,细节以 HarmonyOS API 为准。(华为开发者)

2)查询服务与特征(Services / Characteristics)

// 发现服务 & 取特征
async getCharacteristicsOf(serviceUuid: UUID) {
  const services = await this.gc.getServices() // 返回远端服务列表
  const svc = services.find(s => s.serviceUuid.toLowerCase() === serviceUuid.toLowerCase())
  if (!svc) throw new Error('service not found')
  return svc.characteristics // Array<ble.BLECharacteristic>
}

3)读写特征值(Read/Write)与 MTU

// 读取
async read(serviceUuid: UUID, charUuid: UUID) {
  const value = await this.gc.readCharacteristicValue({ serviceUuid, characteristicUuid: charUuid })
  // value 是 Uint8Array,按协议自行解码(如 UTF-8 / 小端整数)
  return value
}

// 写入(Write With/Without Response)
async write(serviceUuid: UUID, charUuid: UUID, payload: Uint8Array, withRsp = true) {
  await this.gc.writeCharacteristicValue(
    { serviceUuid, characteristicUuid: charUuid, value: payload, writeType: withRsp ? ble.GattWriteType.WRITE : ble.GattWriteType.WRITE_NO_RESPONSE }
  )
}

// MTU(适当增大提升吞吐)
async tuneMtu(size = 185) {
  try { await this.gc.setBLEMtuSize(size) } catch { /* 设备不支持也要兜底 */ }
}

这些接口形态与 @ohos.bluetooth.bleGattClientDevice 文档一致:getServices/readCharacteristicValue/writeCharacteristicValue/setBLEMtuSize 等。不同 API 等级的命名微差以当前参考页为准。(华为开发者)

4)订阅通知/指示(Notify/Indicate)

// 开启通知(或指示)
async enableNotify(serviceUuid: UUID, charUuid: UUID) {
  await this.gc.setCharacteristicChangeNotification({ serviceUuid, characteristicUuid: charUuid, enable: true })
  // 或者设备要求 "Indication":
  // await this.gc.setCharacteristicChangeIndication({ serviceUuid, characteristicUuid: charUuid, enable: true })
  // 之后,变化会走 on('BLECharacteristicChange') 回调
}

注意:只有打开通知/指示以后,BLECharacteristicChange 才会有数据;监听与开启的先后不强制,但“未开启前”不会收到任何回调——开发者常被这个时序细节坑到。(CSDN 博客)


四、把流程拉成“可复用的 BLE 管理器”(能直接贴进项目)

// BleManager.ts —— 扫描 + 连接 + 发现 + 读写/订阅,一条龙
export class BleManager {
  private scanner = new Scanner()
  private client?: GattClient

  onDevices?: (list: FoundDevice[]) => void
  onNotify?: (svc: UUID, ch: UUID, data: Uint8Array) => void

  constructor() {
    this.scanner.onFound = (list) => this.onDevices?.(list)
  }

  async startScan(serviceUuid?: string) {
    await ensureBluetoothOn()
    await this.scanner.start(serviceUuid)
  }

  async stopScan() { await this.scanner.stop() }

  async connect(deviceId: string) {
    this.client = new GattClient(deviceId)
    // 这里把特征变化转给 UI
    this.client['gc'].on('BLECharacteristicChange', (ch) => this.onNotify?.(ch.serviceUuid, ch.characteristicUuid, ch.characteristicValue))
    await this.client.connect()
    await this.client.tuneMtu(185)
  }

  async read(svc: UUID, ch: UUID) { return await this.client!.read(svc, ch) }
  async write(svc: UUID, ch: UUID, bytes: Uint8Array, withRsp = true) { await this.client!.write(svc, ch, bytes, withRsp) }
  async notify(svc: UUID, ch: UUID) { await this.client!.enableNotify(svc, ch) }
  async close() { await this.client?.close(); this.client = undefined }
}

心法总结

  1. 状态机化:所有动作都应该有“当前状态 → 下一步”的判断,否则容易乱序;
  2. 可观测:每个阶段打点(连接耗时、服务发现耗时、Mtu、失败码),线下复现会轻松很多;
  3. 重试策略:连接/读写失败做指数回退,最多 N 次,别无限循环烧电。

五、ArkUI 最小页面:扫、连、读一气呵成(可直接跑)

// pages/BleDemo.ets
import { BleManager } from '../bluetooth/BleManager'

@Entry
@Component
struct BleDemo {
  private mgr = new BleManager()
  @State devices: Array<{ deviceId: string; name: string; rssi: number }> = []
  @State log: string = '…'
  private svc: string = '0000180f-0000-1000-8000-00805f9b34fb' // Battery Service(示例)
  private chr: string = '00002a19-0000-1000-8000-00805f9b34fb' // Battery Level

  aboutToAppear() {
    this.mgr.onDevices = (list) => this.devices = list
    this.mgr.onNotify = (_, __, data) => this.log = `notify: ${Array.from(data).join(',')}`
  }

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        Button('Start Scan').onClick(() => this.mgr.startScan(this.svc))
        Button('Stop Scan').onClick(() => this.mgr.stopScan())
      }

      List() {
        ForEach(this.devices, (d) => ListItem() {
          Row() {
            Text(`${d.name}  ${d.rssi} dBm`).layoutWeight(1)
            Button('Connect').onClick(async () => {
              await this.mgr.stopScan()
              await this.mgr.connect(d.deviceId)
              await this.mgr.notify(this.svc, this.chr)    // 订阅电量特征(示例)
              const val = await this.mgr.read(this.svc, this.chr)
              this.log = `Battery=${val[0]}%`
            })
          }.padding(12)
        }, d => d.deviceId)
      }.height('60%')

      Text(this.log).fontSize(12).opacity(0.8).lineHeight(18)
    }.padding(16)
  }
}

六、常见坑 & 止血锦囊

  1. “已经监听了特征变化,却没回调?”
    → 未调用 setCharacteristicChangeNotification/Indication不会触发;监听和开启的先后没强制,但一定要开启。(华为开发者)
  2. “同一个设备今天能连,明天 deviceId 变了?”
    → 很可能用的是随机地址。用 access.addPersistentDeviceId 建立持久映射,或引导用户在设备端“固定地址/绑定”。(华为开发者)
  3. “开不了蓝牙/权限失败 201”
    → 缺 ohos.permission.ACCESS_BLUETOOTH 或用户拒绝授权;按 @ohos.bluetooth.access 的错误码处理。(华为开发者)
  4. “扫描 API 版本不一致”
    → 新版优先用 createBleScanner();老项目可暂留 startBLEScan,但请计划迁移。(华为开发者)
  5. “吞吐太低 / 包太碎”
    → 先 setBLEMtuSize;协议层再做包聚合重传;通知频率过高也会丢。(华为开发者)
  6. “这设备要配对才让读写”
    → 订阅 bondStateChange + pinRequired,走 connection.pairDevice;经典蓝牙走 socket/SPP,别混用。(码云)

七、扩展一丢丢:经典蓝牙(SPP)与 GATT 的选择题

  • 数据流/串口类设备(打印机、扫码枪、老 MCU)常走 SPP,在 HarmonyOS 用 @ohos.bluetooth.socket(server/client、读写流)。
  • IoT 低功耗/强约定协议设备优先 BLE GATT(服务/特征模型、Notify/Indicate)。
    产品如果两条都要支持,建议将“链路层(GATT/SPP)抽象”为同一接口层,业务上只看“send/receive”。(华为开发者)

八、把“调试体验”做舒服:三件小事

  1. 全链路日志:扫描上报、连接耗时、服务数量、MTU、每次读写字节数与错误码。
  2. 抓包与复盘:必要时接上 BLE 嗅探器,或在外设上打开 HCI log。
  3. 状态机可视化:把状态(Scanning/Connecting/Ready/Retrying…)画在 UI 顶部,一眼定位卡在哪。

参考与进一步阅读(强烈建议收藏)

  • BLE/GATT 开发指南(客户端/服务端、完整流程):官方分步讲解“连接与传输”。(华为开发者)
  • @ohos.bluetooth.ble API 参考(GattClientDevice/Scanner/读写/通知/MTU):具体方法与事件列表、版本标注。(华为开发者)
  • 扫描器 API 变更(createBleScanner 等):了解新旧接口差异,方便迁移。(华为开发者)
  • 蓝牙 access 模块(开关/状态/持久设备 ID)enableBluetoothAsync/getState/addPersistentDeviceId。(华为开发者)
  • 经典蓝牙 Socket(SPP):面向“串口式”通信设备。(华为开发者)

结语:别硬刚,先“排兵布阵”,蓝牙也能稳如老狗 🐶

只要你把权限 → 扫描/配对 → GATT 状态机这条线理顺,HarmonyOS 下的蓝牙开发就会从“玄学”变成“工程”。
  下次有人问“为什么我连上了却收不到数据?”,你就淡定地反问:
“Notify 开了吗?MTU 调了吗?状态机走对了吗?”——把这三板斧挥出去,八成问题当场见光死。

(未完待续)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐