鸿蒙蓝牙通信

https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-bluetooth-low-energy

蓝牙权限

module.json5

bash 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.ACCESS_BLUETOOTH",
        "reason": "$string:app_name",
        "usedScene": {
          "when": "inuse"
        }
      }
    ],
  }
}

蓝牙管理工具类

  1. 申请蓝牙权限 (requestPermission)
    ○ 使用@kit.AbilityKit中的权限管理模块创建AtManager实例。
    ○ 请求用户授权ohos.permission.ACCESS_BLUETOOTH权限。
    ○ 如果用户拒绝,则跳转到设置界面让用户授权。
    ○ 异常时通过promptAction显示错误信息。
  2. 检查蓝牙权限 (checkPermission)
    ○ 获取当前应用的Bundle信息。
    ○ 检查是否已授予蓝牙权限。
    ○ 返回布尔值,表示权限是否被授予。
  3. 检查蓝牙开关状态 (checkOpen)
    ○ 使用@kit.ConnectivityKit的access模块获取蓝牙状态。
    ○ 返回布尔值,表示蓝牙是否开启(返回access.BluetoothState.STATE_ON即开启)。
  4. 扫描蓝牙设备 (scanDevice)
    ○ 启动蓝牙扫描,设置扫描间隔、扫描模式等参数。
    ○ 监听BLEDeviceFind事件,收集扫描到的设备并去重。
    ○ 通过回调函数将扫描结果数组返回给调用者。
    ○ 设置10秒超时自动停止扫描。
  5. 连接蓝牙设备 (connectDevice)
    ○ 根据设备ID创建GATT客户端设备实例。
    ○ 连接设备,并监听连接状态变化事件。
    ○ 当连接成功(状态变为STATE_CONNECTED)时,执行回调函数并显示成功提示,同时调用listenChange方法开启通知监听。
  6. 断开蓝牙连接 (disconnectDevice)
    ○ 如果存在连接,断开并关闭GATT客户端设备,重置引用。
    ○ 显示断开提示。
  7. 发送数据 (sendData)
    ○ 向已连接设备写入数据。
    ○ 查找UUID以0000AE30开头的服务,然后在该服务下查找UUID以0000AE10开头的特征。
    ○ 将JSON格式的数据编码为Uint8Array并写入特征值。
  8. 开启通知监听 (listenChange方法)
    ○ 在连接成功后调用,用于监听特征值变化(通知)。
    ○ 监听BLECharacteristicChange事件,接收到数据时解析为BlueData结构。
    ○ 如果数据命令为wifi,则显示接收到的数据。
    ○ 查找UUID以0000AE30开头的服务,然后查找UUID以0000AE04开头的特征,并启用该特征的通知。

blueManager.ets

bash 复制代码
import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit' // 导入能力访问控制相关模块
import { promptAction } from '@kit.ArkUI' // 导入用户界面交互模块
import { access, ble, constant } from '@kit.ConnectivityKit' // 导入蓝牙相关功能模块
import { util } from '@kit.ArkTS' // 导入文本编码工具
import { emitter } from '@kit.BasicServicesKit'

// 1. 蓝牙开门 2. 配置设备 wifi 连网
interface BlueData {
  status?: 200 | 400 //  200 成功  400 失败
  msg?: string // 消息提示
  command?: 'open' | 'wifi' // 命令类型:开门或配置Wi-Fi
  data?: string[] // 例如配置Wi-Fi时的数据:[ssid, pwd]
}

class BlueManager {
  // 1. 申请蓝牙权限
  async requestPermission() {
    try {
      const AtManager = abilityAccessCtrl.createAtManager() // 创建权限管理器实例
      const res =
        await AtManager.requestPermissionsFromUser(getContext(), ['ohos.permission.ACCESS_BLUETOOTH']) // 向用户请求蓝牙权限
      if (res.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) { // 如果权限被拒绝
        await AtManager.requestPermissionOnSetting(getContext(), ['ohos.permission.ACCESS_BLUETOOTH']) // 在设置中请求权限
      }
    } catch (e) {
      promptAction.showToast({ message: JSON.stringify(e) }) // 显示错误信息
    }
  }

  // 2. 检测蓝牙权限
  checkPermission() {
    const bundleInfo =
      bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION) // 获取当前应用的bundle信息
    const AtManager = abilityAccessCtrl.createAtManager() // 创建权限管理器实例
    const grantStatus =
      AtManager.checkAccessTokenSync(bundleInfo.appInfo.accessTokenId, 'ohos.permission.ACCESS_BLUETOOTH') // 检查蓝牙权限状态
    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED // 返回权限是否被授予
  }

  // 3. 检测蓝牙开关
  checkOpen() {
    return access.getState() === access.BluetoothState.STATE_ON // 检查蓝牙是否开启
  }

  timeoutID: number = 0 // 扫描超时ID,用于控制扫描时长

  // 4. 扫描蓝牙设备
  scanDevice(callback: (res: ble.ScanResult[]) => void) {
    try {
      clearTimeout(this.timeoutID) // 清除之前的超时设置
      const scanList: ble.ScanResult[] = [] // 初始化扫描结果列表
      // 监听蓝牙设备发现事件
      ble.on('BLEDeviceFind', (res: ble.ScanResult[]) => {
        scanList.push(...res.filter(v => !scanList.some(vv => v.deviceId === vv.deviceId))) // 过滤重复设备并更新扫描列表
        callback(scanList) // 调用回调函数传递扫描结果
      })
      // 开始蓝牙扫描
      ble.startBLEScan(null, {
        interval: 500, // 设置扫描间隔时间为500毫秒
        dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER, // 设置低功耗扫描模式
        matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE // 设置积极匹配模式
      })
      this.timeoutID = setTimeout(() => {
        ble.stopBLEScan() // 停止扫描
        ble.off('BLEDeviceFind') // 停止监听蓝牙设备发现事件
      }, 10000)
    } catch (e) {
      promptAction.showToast({ message: JSON.stringify(e) }) // 显示错误信息
    }
  }

  currentClient: ble.GattClientDevice | null = null // 当前连接的蓝牙客户端设备

  // 5. 连接蓝牙设备
  connectDevice(deviceId: string, callback?: () => void) {
    if (deviceId) {
      this.currentClient = ble.createGattClientDevice(deviceId) // 创建Gatt客户端设备实例
      this.currentClient.connect() // 连接到蓝牙设备
      this.currentClient.on('BLEConnectionStateChange', async (res) => {
        if (res.state === constant.ProfileConnectionState.STATE_CONNECTED) { // 如果连接状态为已连接
          callback?.() // 调用回调函数
          promptAction.showToast({ message: '蓝牙连接成功' }) // 显示连接成功提示
          // 监听特征值变化
          this.listenChange()
        }
      })
    }
  }

  // 6. 断开蓝牙连接
  disconnectDevice() {
    if (this.currentClient) {
      this.currentClient.disconnect() // 断开蓝牙连接
      this.currentClient.close() // 关闭客户端设备
      this.currentClient = null // 重置当前客户端设备为null
      promptAction.showToast({ message: '蓝牙已断开' }) // 显示断开连接提示
    }
  }

  // 7. 发送数据
  async sendData(data: BlueData) {
    try {
      if (this.currentClient) {
        const list = await this.currentClient.getServices() // 获取蓝牙设备的所有服务
        const doorService = list.find(v => v.serviceUuid.startsWith('0000AE30')) // 查找门控服务
        const message = doorService?.characteristics.find(v => v.characteristicUuid.startsWith('0000AE10')) // 查找门控特征值
        const encoder = new util.TextEncoder() // 创建文本编码器
        const u8a = encoder.encodeInto(JSON.stringify(data)) // 将数据编码为Uint8Array
        // 写入特征值
        await this.currentClient.writeCharacteristicValue({
          serviceUuid: message?.serviceUuid,
          characteristicUuid: message?.characteristicUuid,
          characteristicValue: u8a.buffer,
          descriptors: [],
        }, ble.GattWriteType.WRITE)
      }
    } catch (e) {
      promptAction.showToast({ message: JSON.stringify(e) }) // 显示错误信息
    }
  }

  // 8. 监听特征值变化
  async listenChange() {
    if (this.currentClient) {
      // 注册特征值变化事件监听器
      this.currentClient?.on('BLECharacteristicChange', (res) => {
        // 创建文本解码器
        const decoder = util.TextDecoder.create()
        // 将特征值数据转换为Uint8Array
        const buffer = new Uint8Array(res.characteristicValue)
        // 将Uint8Array解码为字符串并解析为JSON对象
        const result = JSON.parse(decoder.decodeToString(buffer)) as BlueData
        // 判断命令类型是否为wifi
        if (result.command === 'wifi') {
          // 触发wifi连接事件,并传递状态信息
          promptAction.showToast({ message: JSON.stringify(result, null, 2) })
        }
      })
      // 获取蓝牙设备的服务列表
      const serviceList = await this.currentClient?.getServices()
      // 查找门控服务
      const doorService = serviceList?.find(v => v.serviceUuid.startsWith('0000AE30'))
      // 查找门控特征值
      const message = doorService?.characteristics.find(v => v.characteristicUuid.startsWith('0000AE04'))
      // 启用特征值变化通知
      await this.currentClient?.setCharacteristicChangeNotification(message, true)
    }
  }
}

export const blueManager = new BlueManager() // 导出BlueManager实例

蓝牙测试页面

Test.ets

bash 复制代码
import { blueManager } from '../utils/blueManager'
import { ble } from '@kit.ConnectivityKit'

@Entry
@Component
struct Index {
  @State isPermission: boolean = false
  @State isOpen: boolean = false
  @State list: ble.ScanResult[] = []

  aboutToAppear(): void {
    this.checkPermission()
    this.checkOpen()
  }

  checkPermission() {
    this.isPermission = blueManager.checkPermission()
  }

  checkOpen() {
    this.isOpen = blueManager.checkOpen()
  }

  build() {
    Column({ space: 10 }) {
      Button('申请蓝牙权限')
        .onClick(async () => {
          await blueManager.requestPermission()
          this.checkPermission()
        })
      Button('检测蓝牙权限是否开启:' + this.isPermission)
        .onClick(() => {
          this.isPermission = blueManager.checkPermission()
        })
      Button('检测蓝牙开关是否开启:' + this.isOpen)
        .onClick(() => {
          this.isOpen = blueManager.checkOpen()
        })
      Button('扫描蓝牙设备')
        .onClick(() => {
          blueManager.scanDevice((res) => {
            this.list = res
          })
        })

      List({ space: 5 }) {
        ForEach(this.list, (item: ble.ScanResult) => {
          if (item.deviceName) {
            ListItem() {
              Row() {
                Text(item.deviceName)
                Button('连接蓝牙')
                  .size({ height: 30 })
                  .onClick(() => {
                    blueManager.connectDevice(item.deviceId)
                  })
              }
            }
          }
        })
      }
      .width('100%')
      .height(300)

      Button('断开蓝牙连接')
        .onClick(() => {
          blueManager.disconnectDevice()
        })
      Button('发送数据-解锁开门')
        .onClick(() => {
          blueManager.sendData({
            command: 'open'
          })
        })
      Button('发送数据-配网')
        .onClick(() => {
          blueManager.sendData({
            command: 'wifi',
            data: ['Megasu_iPhone', 'nideshengri'] // 写入自己的 WIFI 和 密码
          })
        })
    }
    .height('100%')
    .width('100%')
  }
}