都 2025 了,蓝牙还连不稳?不如把 GATT 状态机先理顺了再出手!
本文摘要: 文章《零基础学鸿蒙》详细介绍了HarmonyOS蓝牙开发的核心流程,分为权限配置、设备扫描配对和数据传输三大模块。 权限配置:需在module.json5中声明ohos.permission.ACCESS_BLUETOOTH权限,并通过enableBluetoothAsync控制蓝牙开关。 设备扫描与配对:使用BleScanner进行设备扫描,支持服务UUID过滤,并处理设备随机地址问
我是兰瓶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 进化点:新版本引入
BleScanner(createBleScanner()),支持更细的上报模式;老接口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.ble的GattClientDevice文档一致: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 }
}
心法总结
- 状态机化:所有动作都应该有“当前状态 → 下一步”的判断,否则容易乱序;
- 可观测:每个阶段打点(连接耗时、服务发现耗时、Mtu、失败码),线下复现会轻松很多;
- 重试策略:连接/读写失败做指数回退,最多 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)
}
}
六、常见坑 & 止血锦囊
- “已经监听了特征变化,却没回调?”
→ 未调用setCharacteristicChangeNotification/Indication就不会触发;监听和开启的先后没强制,但一定要开启。(华为开发者) - “同一个设备今天能连,明天 deviceId 变了?”
→ 很可能用的是随机地址。用access.addPersistentDeviceId建立持久映射,或引导用户在设备端“固定地址/绑定”。(华为开发者) - “开不了蓝牙/权限失败 201”
→ 缺ohos.permission.ACCESS_BLUETOOTH或用户拒绝授权;按@ohos.bluetooth.access的错误码处理。(华为开发者) - “扫描 API 版本不一致”
→ 新版优先用createBleScanner();老项目可暂留startBLEScan,但请计划迁移。(华为开发者) - “吞吐太低 / 包太碎”
→ 先setBLEMtuSize;协议层再做包聚合与重传;通知频率过高也会丢。(华为开发者) - “这设备要配对才让读写”
→ 订阅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”。(华为开发者)
八、把“调试体验”做舒服:三件小事
- 全链路日志:扫描上报、连接耗时、服务数量、MTU、每次读写字节数与错误码。
- 抓包与复盘:必要时接上 BLE 嗅探器,或在外设上打开 HCI log。
- 状态机可视化:把状态(Scanning/Connecting/Ready/Retrying…)画在 UI 顶部,一眼定位卡在哪。
参考与进一步阅读(强烈建议收藏)
- BLE/GATT 开发指南(客户端/服务端、完整流程):官方分步讲解“连接与传输”。(华为开发者)
@ohos.bluetooth.bleAPI 参考(GattClientDevice/Scanner/读写/通知/MTU):具体方法与事件列表、版本标注。(华为开发者)- 扫描器 API 变更(
createBleScanner等):了解新旧接口差异,方便迁移。(华为开发者) - 蓝牙 access 模块(开关/状态/持久设备 ID):
enableBluetoothAsync/getState/addPersistentDeviceId。(华为开发者) - 经典蓝牙 Socket(SPP):面向“串口式”通信设备。(华为开发者)
结语:别硬刚,先“排兵布阵”,蓝牙也能稳如老狗 🐶
只要你把权限 → 扫描/配对 → GATT 状态机这条线理顺,HarmonyOS 下的蓝牙开发就会从“玄学”变成“工程”。
下次有人问“为什么我连上了却收不到数据?”,你就淡定地反问:
“Notify 开了吗?MTU 调了吗?状态机走对了吗?”——把这三板斧挥出去,八成问题当场见光死。
…
(未完待续)
更多推荐



所有评论(0)