BLE 蓝牙开发使用速查

BLE 蓝牙开发使用速查

项目中 BLE 主要用于:

搜索附近的相机
连接相机蓝牙
发现 GATT 服务
读取相机 Wi-Fi 名称和密码
通过蓝牙打开相机 Wi-Fi
接收相机状态通知
最后切换到 Wi-Fi 连接相机

相关代码:

BleScanManager.ets       -> BLE 扫描
GattClientManager.ets    -> GATT 连接、读写、通知
BluetoothService.ets     -> BLE 服务封装
BluetoothUUID.ets        -> 服务和特征值 UUID
ConnectDeviceViewModel   -> 连接流程和界面状态

1. BLE 连接的完整流程

检查蓝牙是否打开
  -> 申请蓝牙权限
  -> 开始扫描
  -> 过滤相机设备
  -> 连接 deviceId
  -> 监听连接状态
  -> 设置 MTU
  -> 发现 GATT 服务
  -> 读取或写入特征值
  -> 订阅通知
  -> 断开并释放 GATT

相机项目中的特殊流程:

BLE 连接相机
  -> 判断相机是否已被其他设备连接
  -> 判断相机 Wi-Fi 是否打开
  -> 如果没打开,通过 BLE 写入开 Wi-Fi 指令
  -> 读取 Wi-Fi SSID 和密码
  -> 断开 BLE
  -> 连接相机 Wi-Fi

2. 检查蓝牙是否打开

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

const isOpen =
  access.getState() ===
  access.BluetoothState.STATE_ON;

if (!isOpen) {
  ToastUtil.showToast('请打开蓝牙');
  return;
}

项目封装:

const bleManager =
  BleScanManager.instance();

if (!bleManager.isBluetoothOpen()) {
  ToastUtil.showToast('请打开蓝牙');
  return;
}

蓝牙开关打开不代表已经连接设备:

STATE_ON              -> 蓝牙开关已打开
BLE CONNECTED         -> 已连接某个 BLE 设备
GATT 服务发现完成      -> 可以读写特征值

3. 蓝牙权限

项目当前使用:

const permissions: string[] = [
  'ohos.permission.ACCESS_BLUETOOTH'
];

申请权限:

const atManager =
  abilityAccessCtrl.createAtManager();

const result =
  await atManager.requestPermissionsFromUser(
    context,
    ['ohos.permission.ACCESS_BLUETOOTH']
  );

if (result.authResults[0] !== 0) {
  ToastUtil.showToast('没有蓝牙权限');
  return;
}

也可以使用项目已有封装:

const granted =
  await PermissionUtil.requestPermissionsEasy(
    StyleConstants.BLUETOOTH_PERMISSION
  );

if (!granted) {
  ToastUtil.showToast('请开启蓝牙权限');
  return;
}

权限失败时不要直接开始扫描:

// 不建议
ble.startBLEScan(null, options);

正确顺序:

检查蓝牙开关
  -> 申请权限
  -> 权限成功
  -> 开始扫描

不同鸿蒙版本的权限要求可能不同,最终以项目当前 module.json5 和系统版本为准。

4. 开始扫描 BLE 设备

项目扫描入口:

const bleManager =
  BleScanManager.instance();

bleManager.startScan();

底层扫描配置:

const scanOptions: ble.ScanOptions = {
  interval: 0,
  dutyMode:
    ble.ScanDuty.SCAN_MODE_LOW_LATENCY,
  matchMode:
    ble.MatchMode.MATCH_MODE_AGGRESSIVE
};

ble.startBLEScan(
  null,
  scanOptions
);

扫描参数说明:

SCAN_MODE_LOW_LATENCY -> 更快发现设备,但更耗电
MATCH_MODE_AGGRESSIVE -> 更积极匹配设备
interval=0             -> 尽快接收扫描结果

适合连接相机时使用低延时扫描。

5. 过滤相机设备

不要把所有蓝牙设备都显示出来,应该按设备名称或广播数据过滤。

按设备名称过滤:

const supportedNames: string[] = [
  'SJ30',
  'C100P',
  'C110',
  'C300',
  'C400',
  'SJ6P',
  'S10'
];

function isCameraDevice(
  deviceName: string
): boolean {
  const name =
    deviceName.toLowerCase();

  return supportedNames.some((item) => {
    return name.startsWith(
      item.toLowerCase()
    );
  });
}

扫描回调:

const onDeviceFind = (
  reports: Array<ble.ScanResult>
): void => {
  reports.forEach((item) => {
    if (!item.deviceName) {
      return;
    }

    if (!isCameraDevice(item.deviceName)) {
      return;
    }

    console.info(
      `发现相机: ${item.deviceName}`
    );
  });
};

更可靠的方式是同时判断:

设备名称
厂商数据
Service UUID
可连接状态

6. 扫描结果去重

扫描回调会多次返回同一个设备,不能每次都直接添加。

错误写法:

this.deviceList.push(device);

推荐使用 deviceId 去重:

const exists =
  this.deviceList.some((item) => {
    return item.deviceId === device.deviceId;
  });

if (!exists) {
  this.deviceList.push(device);
}

大量设备时使用 Set

private deviceIdSet =
  new Set<string>();

if (!this.deviceIdSet.has(device.deviceId)) {
  this.deviceIdSet.add(device.deviceId);
  this.deviceList.push(device);
}

扫描列表清空时同时清理:

this.deviceIdSet.clear();
this.deviceList = [];

7. 停止扫描

bleManager.stopScan();

底层实现:

ble.off(
  'BLEDeviceFind',
  this.onReceiveEvent
);

ble.stopBLEScan();

页面退出时停止扫描:

aboutToDisappear(): void {
  this.bleManager.stopScan();
}

不停止扫描可能导致:

持续耗电
设备列表重复刷新
退出页面后仍然更新旧页面
再次进入时重复注册回调

扫描超时:

private scanTimer: number = -1;

startScan(): void {
  this.stopScan();

  ble.startBLEScan(
    null,
    scanOptions
  );

  this.scanTimer = setTimeout(() => {
    this.stopScan();

    if (this.deviceList.length === 0) {
      this.scanTimeout = true;
    }
  }, 20000) as number;
}

8. 通过 deviceId 连接 GATT

扫描到设备后,使用 deviceId 连接:

const gattClient =
  ble.createGattClientDevice(
    deviceId
  );

gattClient.connect();

项目封装:

const bluetoothService =
  BluetoothService.instance();

bluetoothService.connect(
  deviceId
);

连接前判断状态:

if (
  bluetoothService.connectionState !==
  BluetoothState.DISCONNECTED
) {
  return;
}

不要重复连接同一个 GATT:

// 不建议
gattClient.connect();
gattClient.connect();

9. 监听 GATT 连接状态

gattClient.on(
  'BLEConnectionStateChange',
  (stateInfo) => {
    switch (stateInfo.state) {
      case 0:
        console.info('BLE 已断开');
        break;

      case 1:
        console.info('BLE 连接中');
        break;

      case 2:
        console.info('BLE 已连接');
        break;

      case 3:
        console.info('BLE 断开中');
        break;
    }
  }
);

项目状态通常是:

0 -> DISCONNECTED
1 -> CONNECTING
2 -> CONNECTED
3 -> DISCONNECTING

注意:

BLE 已连接 != GATT 服务已经准备好

必须等服务发现完成后才能读写特征值。

10. 设置 MTU

连接成功后,项目会设置 MTU:

const MTU_SIZE = 512;

gattClient.setBLEMtuSize(
  MTU_SIZE
);

监听 MTU 协商结果:

gattClient.on(
  'BLEMtuChange',
  (mtu: number) => {
    console.info(
      'MTU 已协商: ' + mtu
    );

    this.discoverServices();
  }
);

MTU 的作用:

一次可以传输更多数据
减少拆包次数
适合传输较长的指令或响应

如果设备不支持 512,不要把 MTU 协商失败当成连接失败:

try {
  gattClient.setBLEMtuSize(512);
} catch (error) {
  LogUtils.warn(
    TAG,
    `MTU 设置失败: ${JSON.stringify(error)}`
  );
}

11. 发现 GATT 服务

连接成功后调用:

const services =
  await gattClient.getServices();

完整写法:

async discoverServices(): Promise<void> {
  if (!this.gattClient) {
    return;
  }

  try {
    const services =
      await this.gattClient.getServices();

    services.forEach((service) => {
      console.info(
        `service=${service.serviceUuid}`
      );

      service.characteristics?.forEach(
        (characteristic) => {
          console.info(
            `characteristic=${
              characteristic.characteristicUuid
            }`
          );
        }
      );
    });
  } catch (error) {
    LogUtils.error(
      TAG,
      `服务发现失败: ${JSON.stringify(error)}`
    );
  }
}

服务发现失败时不要直接读特征值:

// 服务还没发现完成
await readCharacteristic(...);

正确顺序:

connect
  -> BLEConnectionStateChange=2
  -> setBLEMtuSize
  -> getServices
  -> 找到目标 service
  -> 读写 characteristic

12. 项目中的关键 UUID

项目把 UUID 集中放在:

BluetoothUUID.ets

常见服务:

const BLE_UUID = {
  SERVICE_GET_WIFI_INFO:
    'SERVICE_WIFI_INFO_UUID',

  SERVICE_DEVICE_WIFI_STATUS:
    'SERVICE_WIFI_STATUS_UUID',

  SERVICE_DEVICE_CONNECT_STATUS:
    'SERVICE_CONNECT_STATUS_UUID',

  SERVICE_NOTIFY_PAIR:
    'SERVICE_PAIR_UUID'
};

常见特征值:

const BLE_CHAR_UUID = {
  CHAR_WRITE_OPEN_WIFI:
    'CHAR_OPEN_WIFI_UUID',

  CHAR_READ_SSID:
    'CHAR_READ_SSID_UUID',

  CHAR_READ_PASSWORD:
    'CHAR_READ_PASSWORD_UUID',

  CHAR_CAMERA_CONNECT_STATUS:
    'CHAR_CONNECT_STATUS_UUID',

  CHAR_PAIR:
    'CHAR_PAIR_UUID'
};

实际开发时不要在业务代码里到处写 UUID:

// 不建议
const uuid = 'REAL_UUID_VALUE';

统一引用:

import { BLE_UUID } from './BluetoothUUID';

const serviceUuid =
  BLE_UUID.SERVICE_GET_WIFI_INFO;

13. 读取特征值

const characteristic: ble.BLECharacteristic = {
  serviceUuid,
  characteristicUuid,
  characteristicValue: new ArrayBuffer(0),
  descriptors: []
};

const result =
  await gattClient.readCharacteristicValue(
    characteristic
  );

const value = new TextDecoder()
  .decode(
    new Uint8Array(
      result.characteristicValue
    )
  );

项目封装:

const value =
  await BluetoothService.instance()
    .readCharacteristic(
      serviceUuid,
      characteristicUuid
    );

读取之前检查:

if (
  !gattClient ||
  this.connectionState !==
  BluetoothState.CONNECTED
) {
  return '';
}

读不到数据时检查:

设备是否已连接
服务是否发现完成
UUID 是否正确
特征值是否支持 Read
设备是否已经准备好返回数据

14. 写入特征值

写入字符串:

const encoder =
  util.TextEncoder.create('UTF-8');

const characteristic:
  ble.BLECharacteristic = {
    serviceUuid,
    characteristicUuid,
    characteristicValue:
      encoder.encodeInto('1').buffer,
    descriptors: []
  };

await gattClient.writeCharacteristicValue(
  characteristic,
  ble.GattWriteType.WRITE
);

项目封装:

const success =
  await BluetoothService.instance()
    .writeCharacteristic(
      serviceUuid,
      characteristicUuid,
      '1'
    );

相机打开 Wi-Fi 的指令通常类似:

await writeCharacteristic(
  BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
  BLE_UUID.CHAR_WRITE_OPEN_WIFI,
  '1'
);

写入前必须确认:

GATT 已连接
服务已发现
特征值支持 Write
写入内容编码正确

15. 订阅通知或 Indicate

相机需要主动返回状态时,要订阅特征值变化:

const success =
  await BluetoothService.instance()
    .enableIndication(
      serviceUuid,
      characteristicUuid
    );

底层逻辑:

gattClient.on(
  'BLECharacteristicChange',
  onCharacteristicChange
);

gattClient.setCharacteristicChangeIndication(
  characteristic,
  true,
  (error) => {
    if (error) {
      console.error(
        '开启 Indicate 失败'
      );
      return;
    }

    console.info(
      'Indicate 开启成功'
    );
  }
);

收到数据:

const onCharacteristicChange = (
  characteristic: ble.BLECharacteristic
): void => {
  const value =
    new TextDecoder().decode(
      new Uint8Array(
        characteristic.characteristicValue
      )
    );

  console.info(
    'BLE 通知: ' + value
  );
};

注销时必须传入同一个回调:

gattClient.off(
  'BLECharacteristicChange',
  onCharacteristicChange
);

不要重复注册:

// 不建议每次读写都 on 一次
gattClient.on(
  'BLECharacteristicChange',
  callback
);

16. 相机 Wi-Fi 状态处理

相机 BLE 通知内容可能分多次返回:

#ST
1
#ED

不能只判断单次回调:

if (value === '1') {
  // 可能不完整
}

可以先拼接:

private wifiStateValue: string = '';

onWifiStatusChange(
  value: string
): void {
  const current =
    value.toUpperCase();

  this.wifiStateValue += current;

  if (
    this.wifiStateValue.includes(
      '1#ED'
    )
  ) {
    this.wifiStateValue = '';
    this.getWifiInfoAndConnect();
  }
}

特殊相机可能一次返回完整内容:

#ST1#ED

所以需要同时支持:

if (
  value === '#ST1#ED' ||
  this.wifiStateValue.includes('1#ED')
) {
  this.getWifiInfoAndConnect();
}

17. 通过 BLE 打开相机 Wi-Fi

async openCameraWifi(): Promise<void> {
  const success =
    await this.writeCharacteristic(
      BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
      BLE_UUID.CHAR_WRITE_OPEN_WIFI,
      '1'
    );

  if (!success) {
    ToastUtil.showToast(
      '打开相机 Wi-Fi 失败'
    );
    return;
  }

  console.info(
    '打开 Wi-Fi 指令发送成功'
  );
}

更完整的流程:

订阅 Wi-Fi 状态特征值
  -> 写入 "1"
  -> 等待相机返回 Wi-Fi 已开启
  -> 读取 SSID
  -> 读取密码
  -> startScan()
  -> 连接相机 Wi-Fi

18. 通过 BLE 读取 Wi-Fi 信息

async getCameraWifiInfo(): Promise<void> {
  const ssid =
    await BluetoothService.instance()
      .readCharacteristic(
        BLE_UUID.SERVICE_GET_WIFI_INFO,
        BLE_UUID.CHAR_READ_SSID
      );

  const password =
    await BluetoothService.instance()
      .readCharacteristic(
        BLE_UUID.SERVICE_GET_WIFI_INFO,
        BLE_UUID.CHAR_READ_PASSWORD
      );

  if (!ssid || !password) {
    ToastUtil.showToast(
      '没有读取到相机 Wi-Fi 信息'
    );
    return;
  }

  console.info(
    `相机 Wi-Fi 名称: ${ssid}`
  );

  // 不要打印 password
}

读取到信息后,连接 Wi-Fi 前要先扫描:

WifiManager.startScan();

setTimeout(() => {
  WifiManager.connect(
    ssid,
    password,
    3,
    callback
  );
}, 1000);

19. BLE 配对流程

部分相机首次连接时需要配对。

可能收到:

REFUSE
ACCEPT
配对数字

拒绝:

if (value === 'REFUSE') {
  ToastUtil.showToast(
    '相机拒绝蓝牙配对'
  );
  return;
}

接受:

if (value === 'ACCEPT') {
  this.getWifiInfoAndConnect();
  return;
}

收到配对码:

const pairCode =
  extractPairCode(value);

if (pairCode) {
  showDialogInfo({
    title: '验证码',
    message: pairCode,
    confirmText: '取消连接',
    clickConfirm: () => {
      this.cancelPair();
    }
  });
}

取消配对:

await this.writeCharacteristic(
  BLE_UUID.SERVICE_NOTIFY_PAIR,
  BLE_UUID.CHAR_PAIR,
  'PAIR_CANCEL_COMMAND'
);

配对码、设备信息不要写入日志。

20. BLE 断开和资源释放

async disconnect(): Promise<void> {
  if (!this.gattClient) {
    return;
  }

  this.gattClient.off(
    'BLEConnectionStateChange',
    this.onConnectionStateChange
  );

  this.gattClient.off(
    'BLECharacteristicChange',
    this.onCharacteristicChange
  );

  this.gattClient.disconnect();
  this.gattClient.close();

  this.gattClient = undefined;
  this.connectionState =
    BluetoothState.DISCONNECTED;
}

停止连接时要清理:

连接超时定时器
连接状态监听
特征值通知监听
GATT client
服务列表
临时 Wi-Fi 状态
配对弹窗

项目中 disconnect()stopScan() 是两件事:

disconnect() -> 断开当前 GATT
stopScan()   -> 停止搜索附近设备

根据页面需求决定是否同时调用。

21. 连接超时

private connectTimer: number = -1;

startConnect(deviceId: string): void {
  this.connect(deviceId);

  this.connectTimer = setTimeout(() => {
    if (
      this.connectionState !==
      BluetoothState.CONNECTED
    ) {
      this.disconnect();
      this.onConnectTimeout();
    }
  }, 80000) as number;
}

连接成功后清理:

clearTimeout(this.connectTimer);
this.connectTimer = -1;

不要每次重试都创建一个新的定时器而不清理。

22. BLE 扫描和连接常见问题

扫描不到设备

检查:

1. 蓝牙开关是否打开
2. ACCESS_BLUETOOTH 权限是否授予
3. 是否调用 ble.startBLEScan
4. 过滤条件是否太严格
5. 相机是否真的在广播
6. 是否已经被其他设备连接
7. 扫描回调是否重复注册或提前移除

能扫描到但连不上

检查:

1. 使用的是 deviceId,不是 deviceName
2. 是否已经有旧 GATT 连接
3. 是否先 stop 了上一次连接
4. 连接状态是否仍是 CONNECTING
5. 是否同时调用了多个 connect
6. 是否超过连接超时时间

GATT 已连接但读不到数据

检查:

1. 是否已经 getServices
2. service UUID 是否正确
3. characteristic UUID 是否正确
4. 当前特征值是否支持 Read
5. 是否等待 MTU 和服务发现完成

收不到通知

检查:

1. 是否调用 setCharacteristicChangeIndication
2. 是否开启成功
3. 是否监听了正确 characteristic
4. 是否重复注册或提前 off
5. 回调中的 UUID 是否是目标 UUID

BLE 已连接但 Wi-Fi 连接失败

检查:

1. 是否成功读取 SSID
2. 是否成功读取密码
3. 连接 Wi-Fi 前是否调用 startScan
4. 是否等待相机 Wi-Fi 真正开启
5. Wi-Fi 连接后是否等待 DHCP
6. 是否绑定 App 到相机 Wi-Fi

23. 脱敏后的完整 BLE 连接案例

async function connectCameraByBle(
  context: common.UIAbilityContext
): Promise<void> {
  const bluetoothService =
    BluetoothService.instance();

  if (!bluetoothService.isBluetoothOpen()) {
    ToastUtil.showToast('请打开蓝牙');
    return;
  }

  await bluetoothService.startScan(context);

  // 实际项目中应从 deviceList 中选择设备
  const deviceId =
    'BLE_DEVICE_ID';

  if (!deviceId) {
    ToastUtil.showToast('没有找到相机');
    return;
  }

  bluetoothService.connect(deviceId);

  try {
    await bluetoothService.waitForConnected(
      80000
    );

    await bluetoothService.enableIndication(
      BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
      BLE_UUID.CHAR_WRITE_OPEN_WIFI
    );

    await bluetoothService.writeCharacteristic(
      BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
      BLE_UUID.CHAR_WRITE_OPEN_WIFI,
      '1'
    );

    const ssid =
      await bluetoothService.readCharacteristic(
        BLE_UUID.SERVICE_GET_WIFI_INFO,
        BLE_UUID.CHAR_READ_SSID
      );

    const password =
      await bluetoothService.readCharacteristic(
        BLE_UUID.SERVICE_GET_WIFI_INFO,
        BLE_UUID.CHAR_READ_PASSWORD
      );

    if (!ssid || !password) {
      ToastUtil.showToast(
        '读取相机 Wi-Fi 信息失败'
      );
      return;
    }

    bluetoothService.disconnect();
    bluetoothService.stopScan();

    // BLE 连接 Wi-Fi 前先扫描 Wi-Fi
    WifiManager.startScan();

    setTimeout(() => {
      WifiManager.connect(
        ssid,
        password,
        3,
        async (result) => {
          if (
            result ===
            WifiConnectErrorCode
              .WIFI_CONNECT_SUCCESS
          ) {
            await WifiManager.bindToCameraWifi();
            ToastUtil.showToast(
              '相机连接成功'
            );
          }
        }
      );
    }, 1000);
  } catch (error) {
    LogUtils.error(
      TAG,
      `BLE 连接失败: ${JSON.stringify(error)}`
    );

    bluetoothService.disconnect();
    bluetoothService.stopScan();
  }
}

隐私字段统一使用:

BLE_DEVICE_ID
CAMERA_BLE_NAME
CAMERA_WIFI_SSID
CAMERA_WIFI_PASSWORD
CAMERA_GATEWAY
SERVICE_UUID
CHARACTERISTIC_UUID
DEVICE_SERIAL
posted @ 2026-09-01 18:32  带头大哥d小弟  阅读(15)  评论(0)    收藏  举报