健康类 APP 连智能手环读心率、连蓝牙秤读体重、连血糖仪读数据——BLE(低功耗蓝牙)是这些 IoT 场景的通信基础。HarmonyOS NEXT 提供了完整的 BLE API:扫描发现、GATT 连接、服务发现、特征值读写、通知订阅。这篇从零到一讲清楚 BLE 通信全流程。

BLE 通信概览

BLE 通信核心概念:

  • Peripheral(外设)——手环、体重秤等硬件设备,广播自己的存在
  • Central(中心)——手机 APP,扫描并连接外设
  • GATT——通用属性协议,定义数据组织方式(Service → Characteristic)
  • Service——一组相关特征的集合(如"心率服务"0x180D)
  • Characteristic——具体的数据项(如"心率测量"0x2A37),可读/写/通知
import { ble } from '@kit.ConnectivityKit'
import { access } from '@kit.ConnectivityKit'

在这里插入图片描述

权限配置

BLE 需要蓝牙权限,在 module.json5 中声明。

{
  "requestPermissions": [
    { "name": "ohos.permission.ACCESS_BLUETOOTH" }
  ]
}

注意: ACCESS_BLUETOOTH 是 user_grant 权限,需要动态申请。

BLE 扫描

扫描是 BLE 通信的第一步——发现周围的蓝牙设备。

interface BleDevice {
  id: string
  name: string
  rssi: number
  isConnected: boolean
}

@Entry
@Component
struct BleScanDemo {
  @State deviceList: BleDevice[] = []
  @State isScanning: boolean = false
  @State statusMsg: string = ''

  build() {
    Column({ space: 16 }) {
      Text('BLE 扫描')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .width('100%')

      Row({ space: 8 }) {
        Button(this.isScanning ? '扫描中...' : '开始扫描')
          .enabled(!this.isScanning)
          .onClick(() => this.startScan())
        Button('停止扫描')
          .enabled(this.isScanning)
          .onClick(() => this.stopScan())
      }

      ForEach(this.deviceList, (device: BleDevice) => {
        Row({ space: 12 }) {
          Column({ space: 2 }) {
            Text(device.name || '未知设备')
              .fontSize(15)
              .fontWeight(FontWeight.Medium)
            Text(device.id)
              .fontSize(11)
              .fontColor('#999999')
          }
          .layoutWeight(1)
          Text(`RSSI: ${device.rssi}`)
            .fontSize(12)
            .fontColor(device.rssi > -50 ? '#4CAF50' : '#FF9800')
        }
        .width('100%')
        .padding(12)
        .borderRadius(8)
        .backgroundColor('#FAFAFA')
      }, (device: BleDevice) => device.id)

      Text(this.statusMsg)
        .fontSize(13)
        .fontColor('#999999')
    }
    .width('100%')
    .padding(20)
  }

  private startScan(): void {
    this.isScanning = true
    this.statusMsg = '扫描中...(需真机+蓝牙权限)'
    // 真实场景:ble.startBLEScan()
    setTimeout(() => { this.isScanning = false }, 5000)
  }

  private stopScan(): void {
    this.isScanning = false
    // 真实场景:ble.stopBLEScan()
  }
}

真实代码:

ble.startBLEScan(null)
ble.on('BLEDeviceFind', (result: Array<ble.ScanResult>) => {
  for (let i: number = 0; i < result.length; i++) {
    let device: ble.ScanResult = result[i]
    // device.deviceId, device.deviceName, device.rssi
  }
})

要点: startBLEScan 的参数可以传 ScanOptions 过滤特定设备(按 ServiceUUID、设备名等)。扫描到足够设备后及时 stopBLEScan 省电。

GATT 连接

扫描到目标设备后,通过 GATT 连接并发现服务。

// 连接设备
let gattClient: ble.GattClientDevice = ble.createGattClientDevice(deviceId)
await gattClient.connect()

// 发现服务
let services: Array<ble.GattService> = await gattClient.getServices()
for (let i: number = 0; i < services.length; i++) {
  let service: ble.GattService = services[i]
  let chars: Array<ble.GattCharacteristic> = service.characteristics
  // service.serviceUuid, characteristics[].characteristicUuid
}

// 断开连接
gattClient.disconnect()

关键区别: connect() 只是建立连接,getServices() 才能获取设备支持的服务和特征值。必须先发现服务才能读写。

特征值读写

每个 Characteristic 支持三种操作:读、写、通知。

读取特征值

// 读取心率值
let characteristic: ble.GattCharacteristic = {
  serviceUuid: '0000180d-0000-1000-8000-00805f9b34fb',
  characteristicUuid: '00002a37-0000-1000-8000-00805f9b34fb',
  characteristicValue: new ArrayBuffer(0)
}
let result: ble.GattCharacteristic = await gattClient.readCharacteristicValue(characteristic)
// result.characteristicValue 包含心率数据

写入特征值

let writeValue: ble.GattCharacteristic = {
  serviceUuid: '0000180d-0000-1000-8000-00805f9b34fb',
  characteristicUuid: '00002a39-0000-1000-8000-00805f9b34fb',
  characteristicValue: buffer  // 要写入的数据
}
gattClient.writeCharacteristicValue(writeValue, ble.GattWriteType.WRITE_NO_RESPONSE)

订阅通知

心率、步数等实时数据用通知模式——设备主动推,APP 被动收。

gattClient.on('BLECharacteristicChange', (char: ble.GattCharacteristic) => {
  // char.characteristicValue 包含最新数据
  // 解析心率/步数等
})

// 开启通知
let notifyChar: ble.GattCharacteristic = {
  serviceUuid: '0000180d-0000-1000-8000-00805f9b34fb',
  characteristicUuid: '00002a37-0000-1000-8000-00805f9b34fb',
  characteristicValue: new ArrayBuffer(0)
}
await gattClient.setNotifyCharacteristicChanged(notifyChar, true)

要点: setNotifyCharacteristicChanged 开启通知后,设备会在数据变化时主动推送,不需要 APP 反复读取。

BLE 通信全流程 Demo

模拟完整的 BLE 通信流程——扫描→连接→发现服务→读写特征值。

interface ServiceInfo {
  id: string
  uuid: string
  characteristics: string[]
}

@Entry
@Component
struct BleFullDemo {
  @State deviceList: BleDevice[] = []
  @State connectionStatus: string = '未连接'
  @State serviceList: ServiceInfo[] = []
  @State readValue: string = ''
  @State isScanning: boolean = false

  build() {
    Column({ space: 16 }) {
      Text('BLE 通信全流程')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .width('100%')

      // 扫描区
      Row({ space: 8 }) {
        Button(this.isScanning ? '扫描中' : '扫描')
          .onClick(() => this.startScan())
        Button('模拟设备')
          .onClick(() => {
            this.deviceList.push({
              id: `AA:BB:CC:DD:EE:${(this.deviceList.length + 1).toString().padStart(2, '0')}`,
              name: `智能设备-${this.deviceList.length + 1}`,
              rssi: -35 - this.deviceList.length * 10,
              isConnected: false
            })
          })
      }

      // 设备列表
      ForEach(this.deviceList, (device: BleDevice) => {
        Row() {
          Text(`${device.name} (${device.rssi}dBm)`)
            .fontSize(14)
            .layoutWeight(1)
          if (device.isConnected) {
            Text('已连接').fontSize(12).fontColor('#4CAF50')
          } else {
            Button('连接').fontSize(12).height(28)
              .onClick(() => this.connectDevice(device.id))
          }
        }
        .padding(8).borderRadius(6).backgroundColor('#FAFAFA')
      }, (device: BleDevice) => device.id)

      // 服务列表
      if (this.serviceList.length > 0) {
        Text('GATT 服务:')
          .fontSize(16).fontWeight(FontWeight.Medium).width('100%')
        ForEach(this.serviceList, (service: ServiceInfo) => {
          Column({ space: 4 }) {
            Text(`服务: ${service.uuid}`)
              .fontSize(13).fontColor('#1a73e8')
            ForEach(service.characteristics, (char: string) => {
              Row({ space: 8 }) {
                Text(`特征: ${char}`).fontSize(12).layoutWeight(1)
                Button('读').fontSize(11).height(24).padding({ left: 6, right: 6 })
                  .onClick(() => { this.readValue = '72 bpm' })
              }
            }, (char: string) => char)
          }
          .padding(8).borderRadius(6).backgroundColor('#F5F5F5')
        }, (service: ServiceInfo) => service.id)
      }

      if (this.readValue) {
        Text(`读取结果: ${this.readValue}`)
          .fontSize(15).fontColor('#4CAF50')
          .padding(12).borderRadius(8).backgroundColor('#E8F5E9')
      }

      Text(this.connectionStatus)
        .fontSize(13).fontColor('#999999')
    }
    .width('100%')
    .padding(20)
  }

  private startScan(): void {
    this.isScanning = true
    setTimeout(() => { this.isScanning = false }, 3000)
  }

  private connectDevice(deviceId: string): void {
    this.connectionStatus = '连接中...'
    setTimeout(() => {
      this.connectionStatus = '已连接'
      this.serviceList = [
        { id: '1', uuid: '0x180D (心率)', characteristics: ['0x2A37 (心率测量)', '0x2A38 (传感器位置)'] },
        { id: '2', uuid: '0x180A (设备信息)', characteristics: ['0x2A29 (制造商)', '0x2A24 (型号)'] }
      ]
    }, 1500)
  }
}

常见 BLE Service UUID

UUID名称典型特征
0x180D心率服务0x2A37心率测量、0x2A38传感器位置
0x181A环境感知温度、湿度、气压
0x180F电池服务0x2A19电量百分比
0x180A设备信息制造商、型号、固件版本
0x1810血压服务收缩压、舒张压、脉率
0x1818自行车速度速度、踏频

踩坑清单

问题原因解决
startBLEScan 报错未申请蓝牙权限module.json5 声明 + 动态申请
扫描不到设备蓝牙未开启或定位未开检查蓝牙和定位开关
connect 后无服务未调用 getServicesconnect 后必须 getServices
readCharacteristicValue 报错特征不支持读检查 properties.read 是否为 true
通知收不到未 setNotifyCharacteristicChanged先 setNotify(true) 再等推送
断开后重连失败未释放 GattClientdisconnect 后重新 createGattClientDevice
RSSI 值跳变大信号不稳定多次扫描取平均值
写入后设备无响应写入类型不对区分 WRITE/WRITE_NO_RESPONSE
连接超时设备距离远或广播间隔大靠近设备,增大扫描时间
多设备同时连接卡顿GATT 操作串行避免并发操作,排队执行
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐