BLE蓝牙基础通信:扫描、连接、收发数据,IoT简易Demo

蓝牙低功耗(BLE)是鸿蒙IoT开发里最常用的近场通信方式:智能灯泡、手环、传感器、门锁,几乎所有硬件设备都支持BLE连接。很多开发者第一次接蓝牙时,总觉得流程绕——扫描、连接、发现服务、读特征值、写数据、开通知,一套下来步骤多,稍不注意就卡住。本文基于最新鸿蒙API,把BLE通信的完整流程拆成几个环节讲清楚,最后给一个控制灯泡开关的简易Demo,代码可以直接复制到项目里跑。

01 权限前置与蓝牙开关

BLE属于敏感硬件权限,必须先在module.json5声明,再动态申请。很多人代码写得没问题,结果一直扫描不到设备,十有八九是权限没配或者蓝牙开关没打开。需要的权限有两个:

  • ohos.permission.ACCESS_BLUETOOTH:基础蓝牙权限,用于扫描和连接

  • ohos.permission.APPROXIMATELY_LOCATION:模糊定位权限,Android 12以上和鸿蒙都要求,扫描BLE设备必须要有定位权限

module.json5里这样配置:

"requestPermissions": [
  {
    "name": "ohos.permission.ACCESS_BLUETOOTH",
    "reason": "$string:ble_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "$string:location_reason",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

代码里先判断蓝牙开关是否打开,没开的话引导用户去系统设置打开:

import { bluetooth } from '@kit.ConnectivityKit';

// 获取蓝牙代理对象
const bleManager: bluetooth.BLECentralManager = bluetooth.getBLECentralManager();

// 检查蓝牙是否开启
function isBluetoothEnabled(): boolean {
  const state = bleManager.getState();
  return state === bluetooth.BluetoothState.TURNED_ON;
}

// 如果没开,跳转到系统蓝牙设置页
function openBluetoothSettings() {
  // 这里可以通过 want 跳转系统蓝牙设置界面
  console.info('请先打开蓝牙开关');
}

权限申请和蓝牙开关检查要在扫描之前做,不然调用扫描接口直接报错。

02 扫描周边BLE设备

扫描是BLE通信的第一步,打开手机蓝牙后,周围所有正在广播的BLE设备都会被扫到。扫描结果是异步返回的,需要注册回调。

import { bluetooth } from '@kit.ConnectivityKit';

interface ScannedDevice {
  deviceId: string;
  name: string;
  rssi: number; // 信号强度
}

const deviceList: ScannedDevice[] = [];

// 注册扫描结果回调
bleManager.on('BLEDeviceFind', (data: bluetooth.ScanResult[]) => {
  data.forEach(item => {
    const device = item.device;
    const name = device.name || '未知设备';
    // 去重,同一个设备只加一次
    if (!deviceList.find(d => d.deviceId === device.address)) {
      deviceList.push({
        deviceId: device.address,
        name: name,
        rssi: item.rssi
      });
      console.info('发现设备:', name, device.address, '信号:', item.rssi);
    }
  });
});

// 开始扫描
function startScan() {
  const filter: bluetooth.ScanFilter = {
    // 可以按服务UUID过滤,只扫特定设备
    // serviceUuid: '0000fff0-0000-1000-8000-00805f9b34fb'
  };
  bleManager.startBLEScan([filter]);
}

// 停止扫描,扫描很耗电,扫到目标设备后立刻停
function stopScan() {
  bleManager.stopBLEScan();
}

几个关键点:

  • 扫描很耗电,不要一直扫。一般扫10秒左右,找到目标设备就立刻stopScan

  • 信号强度rssi,数值越大信号越好(-40左右很好,-80以下就比较远了)

  • 过滤条件,如果知道目标设备的服务UUID,加上filter可以快速过滤掉无关设备,减少回调数量

  • 去重,同一个设备会反复上报,必须按deviceId去重,不然列表会刷个不停

03 连接目标设备

扫到目标设备后,就可以发起连接。连接是异步的,需要监听连接状态变化。

let gattClient: bluetooth.GATTClient | null = null;

// 监听连接状态变化
bleManager.on('BLEConnectionStateChange', (state: bluetooth.ConnectionState) => {
  console.info('连接状态变化:', state.state);
  if (state.state === bluetooth.ProfileConnectionState.CONNECTED) {
    console.info('设备已连接,deviceId:', state.deviceId);
  } else if (state.state === bluetooth.ProfileConnectionState.DISCONNECTED) {
    console.info('设备已断开');
    gattClient = null;
  }
});

// 发起连接
async function connectDevice(deviceId: string) {
  // 创建GATT客户端
  gattClient = await bleManager.createGATTClient(deviceId);
  // 连接,autoConnect设为false表示立即连接
  await gattClient.connect({
    autoConnect: false,
    transport: bluetooth.Transport.TRANSPORT_LE
  });
}

连接成功之后,下一步就是发现服务。BLE的通信结构是:一个设备包含多个Service(服务),每个Service包含多个Characteristic(特征值),所有数据收发都是针对特征值的。

04 发现服务与特征值

连接成功后,必须先discoverServices,系统才会把设备的服务列表返回给你。不调用这个接口,后面所有读写操作都会失败。

interface BLEService {
  uuid: string;
  characteristics: BLEChar[];
}

interface BLEChar {
  uuid: string;
  properties: string[]; // read / write / notify
}

const services: BLEService[] = [];

async function discoverServices() {
  if (!gattClient) return;
  // 发现所有服务
  await gattClient.discoverServices();
  const gattServices = gattClient.getServices();

  gattServices.forEach(service => {
    const svc: BLEService = {
      uuid: service.uuid,
      characteristics: []
    };
    // 遍历每个服务下的特征值
    const chars = service.getCharacteristics();
    chars.forEach(char => {
      svc.characteristics.push({
        uuid: char.uuid,
        properties: [
          char.properties.read ? 'read' : '',
          char.properties.write ? 'write' : '',
          char.properties.notify ? 'notify' : ''
        ].filter(Boolean)
      });
    });
    services.push(svc);
    console.info('服务:', service.uuid);
  });
}

拿到服务列表后,就可以根据业务需要找到对应的特征值。比如智能灯泡的控制特征值UUID是已知的,直接定位到那个UUID就行。

05 收发数据:读、写、通知

BLE数据收发有三种方式,对应特征值的三种属性:

  • read(读):主动读取一次特征值的当前值,比如读取传感器的当前温度

  • write(写):向特征值写入数据,比如发送控制命令让灯泡开关

  • notify(通知):设备主动向手机推送数据,比如心率手环实时上报心率

async function readCharacteristic(serviceUuid: string, charUuid: string) {
  if (!gattClient) return;
  const service = gattClient.getService(serviceUuid);
  const char = service.getCharacteristic(charUuid);
  // 读取特征值
  const value = await gattClient.readCharacteristic(char);
  // value是ArrayBuffer,转成Uint8Array处理
  const data = new Uint8Array(value);
  console.info('读到数据:', data);
}
async function writeCharacteristic(serviceUuid: string, charUuid: string, data: Uint8Array) {
  if (!gattClient) return;
  const service = gattClient.getService(serviceUuid);
  const char = service.getCharacteristic(charUuid);
  // 写入数据,writeType设为WRITE_TYPE_DEFAULT表示需要设备应答
  await gattClient.writeCharacteristic(char, data.buffer, {
    writeType: bluetooth.GATTWriteType.WRITE_TYPE_DEFAULT
  });
  console.info('写入成功');
}
// 监听通知数据
gattClient.on('BLECharacteristicChange', (char: bluetooth.GATTCharacteristic) => {
  const data = new Uint8Array(char.value);
  console.info('收到通知数据:', data);
});

async function enableNotify(serviceUuid: string, charUuid: string) {
  if (!gattClient) return;
  const service = gattClient.getService(serviceUuid);
  const char = service.getCharacteristic(charUuid);
  // 开启通知
  await gattClient.setCharacteristicNotify(char, true);
  // 还需要写描述符,很多设备要求这一步
  const desc = char.getDescriptor('00002902-0000-1000-8000-00805f9b34fb');
  if (desc) {
    await gattClient.writeDescriptor(desc, new Uint8Array([1, 0]).buffer);
  }
}

数据格式一般是十六进制字节数组,具体怎么解析要看设备的通信协议。比如控制灯泡的命令可能是0x01表示开,0x00表示关,这个要跟硬件工程师确认。

06 IoT简易Demo:控制智能灯泡

把上面的流程串起来,就是一个完整的智能灯泡控制Demo。核心逻辑:扫描设备 → 连接 → 发现服务 → 找到控制特征值 → 写入开关命令。

import { bluetooth } from '@kit.ConnectivityKit';

const bleManager = bluetooth.getBLECentralManager();
let gattClient: bluetooth.GATTClient | null = null;

// 灯泡服务和特征值UUID(实际项目中从设备协议文档获取)
const LIGHT_SERVICE_UUID = '0000fff0-0000-1000-8000-00805f9b34fb';
const LIGHT_WRITE_CHAR_UUID = '0000fff1-0000-1000-8000-00805f9b34fb';

// 扫描并连接目标设备
async function connectLightDevice(targetName: string) {
  bleManager.on('BLEDeviceFind', async (results: bluetooth.ScanResult[]) => {
    for (const item of results) {
      if (item.device.name === targetName) {
        bleManager.stopBLEScan();
        gattClient = await bleManager.createGATTClient(item.device.address);
        await gattClient.connect({ autoConnect: false });
        await gattClient.discoverServices();
        console.info('灯泡已连接,可以控制了');
        break;
      }
    }
  });
  bleManager.startBLEScan([]);
}

// 开灯
async function turnOnLight() {
  if (!gattClient) return;
  const service = gattClient.getService(LIGHT_SERVICE_UUID);
  const char = service.getCharacteristic(LIGHT_WRITE_CHAR_UUID);
  // 发送开灯命令:0x01
  const cmd = new Uint8Array([0x01]);
  await gattClient.writeCharacteristic(char, cmd.buffer);
  console.info('已开灯');
}

// 关灯
async function turnOffLight() {
  if (!gattClient) return;
  const service = gattClient.getService(LIGHT_SERVICE_UUID);
  const char = service.getCharacteristic(LIGHT_WRITE_CHAR_UUID);
  // 发送关灯命令:0x00
  const cmd = new Uint8Array([0x00]);
  await gattClient.writeCharacteristic(char, cmd.buffer);
  console.info('已关灯');
}

// 断开连接,页面退出时调用
async function disconnect() {
  if (gattClient) {
    await gattClient.disconnect();
    gattClient.close();
    gattClient = null;
  }
}

UI层就两个按钮,开灯和关灯,点击分别调用turnOnLight和turnOffLight就行。实际项目中,连接状态、设备列表可以做成列表页,用户选择设备后进入控制页。

小结

BLE通信的流程其实不复杂,核心就是六步:权限检查 → 扫描设备 → 建立连接 → 发现服务 → 定位特征值 → 读写数据。容易卡的地方主要是权限配置不全、扫描忘记停止、连接成功后没discoverServices、开通知没写描述符。把这些步骤封装成一个统一的BLE工具类,项目里直接调用,不用每次都从头写一遍。实际接硬件的时候,关键是拿到设备的通信协议文档,确认服务UUID、特征值UUID和数据格式,剩下的就是照着流程走。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐