HarmonyOS技术精讲-Connectivity Kit:蓝牙基础——经典蓝牙与BLE入门

HarmonyOS技术精讲-Connectivity Kit:蓝牙基础------经典蓝牙与BLE入门

HarmonyOS NEXT 在蓝牙模块上做了比较大的重构,把之前的 @ohos.bluetooth 拆成了两个独立的Manager:classicBluetoothManagerbleManager。这个改动本身是合理的------经典蓝牙和BLE的使用场景、API设计差别很大,拆开之后接口更加清晰。但很多初学者第一次接触时会困惑:到底该用哪个Manager?同一个设备能不能同时用两种模式?

这篇文章从实际开发角度出发,先把这两种模式讲清楚,然后分别给出BLE扫描连接和经典蓝牙文件发送的示例代码。代码可以直接跑,重点会放在生命周期管理和状态同步上,这是最容易出问题的地方。

蓝牙BR/EDR与BLE:先搞清楚用哪个

蓝牙本身有两种工作模式:BR/EDR(经典蓝牙)BLE(低功耗蓝牙)。在HarmonyOS中,它们被封装为两个独立的模块,API风格完全不同。

特性 BLE (bleManager) 经典蓝牙 (classicBluetoothManager)
功耗 极低,纽扣电池可运行数年 较高,适合持续数据传输
数据传输 速度慢(约1Mbps),适合小数据包 速度快(可达2-3Mbps),适合文件/音频
连接方式 广播-扫描-连接-服务发现 配对-连接-RFCOMM通道
典型场景 传感器、心率计、设备发现 耳机、音箱、文件传输
硬件要求 所有蓝牙4.0+设备都支持 必须支持BR/EDR(很多耳机/音箱仅支持此模式)
HarmonyOS API @kit.ConnectivityKitbleManager @kit.ConnectivityKitclassicBluetoothManager

实际开发中,必须先确定业务场景,再选Manager。如果一个设备需要同时支持两种模式(比如既做数据采集,又做文件传输),需要分别初始化两个Manager,它们之间没有依赖关系。

环境说明

复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:Mate 40 Pro (HarmonyOS NEXT)

核心实现:BLE扫描与连接

Step 1:权限声明与动态申请

蓝牙操作需要两个关键权限:ohos.permission.BLUETOOTH(蓝牙基础操作)和 ohos.permission.DISCOVER_BLUETOOTH(扫描发现设备)。在HarmonyOS NEXT中,这两个权限都需要在 module.json5 中声明,并且在运行时动态申请。

json 复制代码
// module.json5
"requestPermissions": [
  {
    "name": "ohos.permission.BLUETOOTH",
    "reason": "$string:app_name"
  },
  {
    "name": "ohos.permission.DISCOVER_BLUETOOTH",
    "reason": "$string:app_name"
  }
]

动态申请代码:

typescript 复制代码
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';

async function requestBlePermissions(context: common.UIAbilityContext): Promise<void> {
  const atManager = abilityAccessCtrl.createAtManager();
  const permissions: Array<Permissions> = [
    'ohos.permission.BLUETOOTH',
    'ohos.permission.DISCOVER_BLUETOOTH'
  ];

  try {
    for (const perm of permissions) {
      const result = await atManager.requestPermissionsFromUser(context, [perm]);
      if (result.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        console.error('权限被拒绝:', perm);
        // 这里建议弹窗提示用户手动授权
      }
    }
  } catch (err) {
    console.error('权限请求失败:', err.message);
  }
}

注意 :权限申请必须放在 aboutToAppear 或者页面初始化阶段,不要在 build() 里申请,否则会导致连续弹窗。另外,requestPermissionsFromUser 是异步的,调用 startBLEScan 之前必须确保权限已经获得。

Step 2:初始化BLE Manager并开始扫描

bleManager 是单例对象,直接通过 import 导入即可使用,不需要 getInstance 之类的方法。

typescript 复制代码
import { ble } from '@kit.ConnectivityKit';

let scanResults: ble.ScanResult[] = [];
let scanning = false;

function startBleScan(): void {
  if (scanning) {
    console.warn('扫描正在进行中,无需重复启动');
    return;
  }

  const filters: ble.ScanFilter[] = [];
  // 可以根据需要设置过滤条件,比如只扫描特定服务UUID的设备
  // filters.push({ serviceUuid: '0000180f-0000-1000-8000-00805f9b34fb' });

  const options: ble.ScanOptions = {
    interval: 500,  // 扫描间隔,单位ms
    dutyMode: ble.ScanDuty.SCAN_MODE_LOW_POWER,  // 功耗模式
    phyType: ble.PhyType.PHY_LE_1M  // 物理层类型
  };

  try {
    ble.startBLEScan(filters, options);
    scanning = true;
    console.info('BLE扫描已启动');
  } catch (err) {
    console.error('startBLEScan失败:', err.message);
  }
}

// 注册扫描结果监听
ble.on('BLEDeviceFind', (data: Array<ble.ScanResult>) => {
  data.forEach(device => {
    const exist = scanResults.some(item => item.deviceId === device.deviceId);
    if (!exist) {
      scanResults.push(device);
      console.info('发现新设备:', device.deviceName, device.deviceId);
    }
  });
});

// 停止扫描
function stopBleScan(): void {
  if (!scanning) return;
  try {
    ble.stopBLEScan();
    scanning = false;
    console.info('BLE扫描已停止');
  } catch (err) {
    console.error('stopBLEScan失败:', err.message);
  }
}

几个关键点

  1. startBLEScanfilters 参数可以传空数组,表示扫描所有设备。如果加了过滤,发现速度会变慢,但能减少无效回调。
  2. on('BLEDeviceFind') 的回调会在每次发现设备时触发,而且同一个设备可能被多次发现。上面代码里用 deviceId 去重,避免UI列表重复渲染。
  3. 扫描间隔 interval 默认是200ms,实测发现设为500ms以上能明显减少设备间的干扰,适合在信号较差的场景。

Step 3:连接设备并监听GATT连接状态

扫描到设备后,调用 connect 进行连接。注意:BLE连接是异步的,需要通过回调监听连接状态。

typescript 复制代码
import { connectedAble, gatt } from '@kit.ConnectivityKit';
// 注意:bleManager也提供了连接方法,但推荐使用connectedAble,它暴露了更详细的连接状态

let deviceId: string; // 从扫描结果中获取

// 连接设备
function connectBleDevice(targetId: string): void {
  deviceId = targetId;

  // 先注册连接状态变化监听
  ble.on('BLEConnectionStateChange', (state: ble.BLEConnectionChangeState) => {
    console.info('连接状态变化: deviceId=', state.deviceId, 'state=', state.state);
    // state.state: 0=已断开, 1=正在连接, 2=已连接
    if (state.state === 2) {
      console.info('设备已连接,开始发现服务');
      discoverServices();
    } else if (state.state === 0) {
      console.warn('设备已断开');
      // 这里建议触发重连逻辑,或者更新UI状态
    }
  });

  try {
    ble.connect(deviceId);
  } catch (err) {
    console.error('连接失败:', err.message);
  }
}

// 发现服务
function discoverServices(): void {
  try {
    const services = ble.getServices(deviceId); // 返回GattService数组
    services.forEach(service => {
      console.info('发现服务:', service.serviceUuid);
      // 可以在这里遍历characteristics,找到需要的特征
    });
  } catch (err) {
    console.error('getServices失败:', err.message);
  }
}

// 断开连接(页面销毁时一定要调用)
function disconnectBleDevice(): void {
  try {
    ble.disconnect(deviceId);
    // 同时注销监听
    ble.off('BLEConnectionStateChange');
  } catch (err) {
    console.error('断开连接失败:', err.message);
  }
}

很多人会忽略 off 操作。如果页面销毁时不注销监听,下次进入页面会重复注册,导致回调被多次触发,出现状态错乱。

核心实现:经典蓝牙配对与文件发送

经典蓝牙的场景通常是设备配对后,通过RFCOMM通道传输文件。HarmonyOS的 classicBluetoothManager 提供了 createBond 进行配对,以及 listener('bondStateChange') 监听配对状态。

Step 1:初始化并打开蓝牙

typescript 复制代码
import { classicBluetooth, baseProfile } from '@kit.ConnectivityKit';

// 获取经典蓝牙实例
const btManager = classicBluetooth.getClassicBluetoothManager();

// 检查并打开蓝牙
function enableClassicBt(): void {
  const state = btManager.getState();
  if (state === baseProfile.BluetoothState.STATE_OFF) {
    btManager.enableBluetooth(); // 注意:这个是异步的,需要轮询状态确认
    console.info('正在打开经典蓝牙...');
  } else {
    console.info('经典蓝牙已开启');
  }
}

经典蓝牙的 enableBluetooth 不会立即生效,建议通过 on('stateChange') 监听状态变化:

typescript 复制代码
btManager.on('stateChange', (data: baseProfile.StateChangeParam) => {
  if (data.state === baseProfile.BluetoothState.STATE_ON) {
    console.info('经典蓝牙已就绪');
    // 可以开始扫描
    startClassicScan();
  }
});

Step 2:扫描经典蓝牙设备并配对

经典蓝牙的扫描方法和BLE不同,它通过 startBluetoothDiscovery 发现设备,然后通过 createBond 建立配对。

typescript 复制代码
let classicDevices: classicBluetooth.BondDevice[] = [];

// 开始扫描
function startClassicScan(): void {
  try {
    btManager.startBluetoothDiscovery(); // 扫描附近的经典蓝牙设备
  } catch (err) {
    console.error('扫描启动失败:', err.message);
  }
}

// 监听发现设备
btManager.on('deviceFind', (device: classicBluetooth.BondDevice) => {
  const exist = classicDevices.some(d => d.deviceId === device.deviceId);
  if (!exist) {
    classicDevices.push(device);
    console.info('发现经典蓝牙设备:', device.deviceName);
    // 可以在这里自动发起配对,或等待用户点击
    // bondWithDevice(device.deviceId);
  }
});

// 发起配对
function bondWithDevice(deviceId: string): void {
  try {
    btManager.createBond(deviceId);
    console.info('正在发起配对...');
  } catch (err) {
    console.error('createBond失败:', err.message);
  }
}

// 监听配对状态
btManager.on('pinRequired', (deviceId: string, pinCode: number) => {
  console.info('需要输入PIN码:', pinCode);
  // 通常需要显示一个UI弹窗让用户输入,或者自动确认
  btManager.setDevicePin(deviceId, pinCode.toString()); // 自动配对
});

btManager.on('bondStateChange', (data: baseProfile.BondStateParam) => {
  if (data.state === baseProfile.BondState.BONDED) {
    console.info('配对成功:', data.deviceId);
    // 配对成功后可以建立RFCOMM连接
  } else if (data.state === baseProfile.BondState.BONDING) {
    console.info('正在配对中...');
  } else if (data.state === baseProfile.BondState.UNBONDED) {
    console.info('配对失败或已解除:', data.deviceId);
  }
});

Step 3:RFCOMM数据传输示例

经典蓝牙的文件传输通常使用RFCOMM(串行端口协议)。下面是一个发送文本数据的示例:

typescript 复制代码
import { socket } from '@kit.ConnectivityKit';
import { fileIo } from '@kit.CoreFileKit';

// 建立RFCOMM连接
async function connectRfcomm(deviceId: string, channel: number): Promise<void> {
  const rfcommSocket = socket.constructRfcommSocketInstance();

  // 监听连接状态
  rfcommSocket.on('connect', () => {
    console.info('RFCOMM连接成功');
    sendFileViaRfcomm(rfcommSocket);
  });

  rfcommSocket.on('disconnect', (err) => {
    console.warn('RFCOMM连接断开:', err.message);
  });

  try {
    await rfcommSocket.connect({ deviceId, channel });
  } catch (err) {
    console.error('RFCOMM连接失败:', err.message);
  }
}

// 发送文件
async function sendFileViaRfcomm(socket: socket.RfcommSocket): Promise<void> {
  const filePath = '/data/storage/el2/base/haps/entry/files/test.txt'; // 模拟一个文件路径
  try {
    const file = await fileIo.open(filePath, fileIo.OpenMode.READ_ONLY);
    const buf = new ArrayBuffer(1024);
    let totalBytes = 0;

    // 分批读取并发送
    while (true) {
      const readResult = await fileIo.read(file.fd, buf);
      if (readResult.bytesRead === 0) break; // 读取完毕
      const sent = socket.send(new Uint8Array(buf.slice(0, readResult.bytesRead)));
      totalBytes += sent;
    }

    await fileIo.close(file.fd);
    console.info('文件发送完成, 共发送字节:', totalBytes);
  } catch (err) {
    console.error('文件发送失败:', err.message);
  }
}

注意 :RFCOMM的 channel 参数需要提前知道,或者通过SDP(服务发现协议)查询。实际项目中,通常需要双方约定一个固定的channel编号。

常见问题

问题1:权限声明后仍然报错 "Permission denied"

现象 :在 module.json5 中添加了权限,运行时 startBLEScan 依然抛异常。

原因:HarmonyOS NEXT 对敏感权限要求必须同时满足两个条件:

  1. module.json5 中声明
  2. 在运行时通过 abilityAccessCtrl 动态申请

如果忘记动态申请,或者用户在权限弹窗中点击了"拒绝",后续接口会持续报错。

解决方案

  • aboutToAppear 中调用权限申请方法,并在回调中处理拒绝情况
  • 如果用户拒绝,引导用户去系统设置中手动开启

问题2:BLE扫描发现设备后,立即连接失败

现象 :扫描日志显示找到了设备,但调用 ble.connect 后回调状态为 "断开"。

原因:BLE设备在广播时处于可连接状态,但如果设备已经与另一个主机建立了连接,或者设备本身处于非连接广播模式(如仅广播数据),则连接会失败。此外,扫描停止后立即连接的成功率更高,因为扫描会占用蓝牙资源。

解决方案

  • 在连接前先调用 ble.stopBLEScan,释放扫描资源
  • 添加重连逻辑:第一次失败后,等待1-2秒再重试一次

问题3:经典蓝牙配对成功后,RFCOMM连接超时

现象createBond 回调了 BONDED,但调用 rfcommSocket.connect 时一直超时。

原因:配对成功只代表设备建立了信任关系,但不代表RFCOMM服务已经就绪。有些设备需要先建立连接(socket),配对才会正式完成。此外,channel号如果不正确,连接也会失败。

解决方案

  • 确保 channel 参数使用的是对方设备支持的RFCOMM通道号
  • 配对成功后,等待500ms-1s再发起RFCOMM连接,给设备足够的准备时间

最佳实践

  1. 不要在 build() 中直接调用 startBLEScan 。ArkUI的 build() 方法会多次执行,每次都会启动新的扫描,导致回调混乱。应该把扫描逻辑放在 aboutToAppear 或者按钮点击事件里。

  2. 页面销毁时必须注销所有监听on('BLEDeviceFind')on('BLEConnectionStateChange') 等监听如果不注销,当页面跳转后,回调节点依然存在,可能会操作已经被销毁的UI组件,导致 undefined 错误。

  3. BLE扫描的生命周期管理 。如果应用进入后台,系统会暂停BLE扫描。建议在 onPageHide 中停止扫描,在 onPageShow 中重新启动。这样可以节省电量,同时避免在后台无效扫描。

FAQ

Q:为什么真机正常,模拟器不生效?

A:HarmonyOS模拟器对蓝牙硬件的模拟不完整,特别是BLE的广播和扫描功能。建议所有蓝牙相关功能都在真机上测试。

Q:为什么页面返回后,再次进入发现设备列表清空了?

A:因为 scanResults 是组件内的局部变量,页面销毁后重新创建,数据自然丢失。建议把扫描结果存储在 AppStorage 或者全局单例中,实现跨页面持久化。

Q:第一次配对成功后,再次连接需要重新配对吗?

A:不需要。配对信息保存在系统蓝牙配置中,再次连接时直接调用 ble.connectrfcommSocket.connect 即可,系统会自动使用已保存的配对凭证。但注意,如果用户在系统蓝牙设置中手动删除了配对,则需要重新 createBond

相关推荐
不羁的木木12 小时前
HarmonyOS技术精讲-Connectivity Kit:自动设备发现与一键配网
华为·harmonyos
雪芽蓝域zzs12 小时前
HarmonyOS开发 指纹验证和ESDSA加密
华为·harmonyos
念雨思13 小时前
HarmonyOS AI 应用开发实战:短视频选题灵感 —— AI 驱动的内容创作引擎
人工智能·学习·华为·harmonyos·鸿蒙
tyqtyq2216 小时前
HarmonyOS AI 应用开发实战:考研择校分析系统
人工智能·学习·考研·华为·生活·harmonyos
ldsweet16 小时前
《HarmonyOS技术精讲-Basic Services Kit》USB服务:设备发现与通信
华为·harmonyos
程序员黑豆17 小时前
从零开始:编写第一个鸿蒙(HarmonyOS)程序
前端·harmonyos
ldsweet17 小时前
《HarmonyOS技术精讲-Basic Services Kit》升级服务:应用内更新检测与下载安装
华为·harmonyos
解局易否结局17 小时前
ArkUI MVI 架构实战:单向数据流设计模式在 HarmonyOS NEXT 中的深度实践
华为·设计模式·架构·harmonyos
信仰87418 小时前
HCIA-华为数通基础理论与实践10
网络·华为·智能路由器