【鸿蒙】HarmonyOS 蓝牙/NFC 短距通信完全指南:从扫描到数据交换的全流程实战
HarmonyOS 蓝牙/NFC 短距通信完全指南从扫描到数据交换的全流程实战掌握 HarmonyOS NEXT 蓝牙BLE/经典蓝牙与 NFCNDEF/卡模拟核心 API彻底解决设备发现、配对、数据收发、权限申请等高频开发痛点。 适用版本HarmonyOS NEXT / API 12 预计阅读时长18 分钟---一、短距通信能力全景短距通信在物联网、支付、设备协同场景中不可或缺。HarmonyOS NEXT 提供两条独立技术栈┌────────────────────────────────────────────────────────┐│ HarmonyOS 短距通信栈 │├─────────────────────┬──────────────────────────────────┤│ 蓝牙子系统 │ NFC 子系统 ││ ┌───────────────┐ │ ┌─────────────────────────────┐ ││ │ 经典蓝牙(BR/EDR)│ │ │ NDEF 标签读写 │ ││ │ - A2DP/AVRCP │ │ │ - NdefMessage/NdefRecord │ ││ │ - SPP 串口 │ │ ├─────────────────────────────┤ ││ ├───────────────┤ │ │ HCE 卡模拟 │ ││ │ BLE 低功耗 │ │ │ - CardEmulation │ ││ │ - GATT Server │ │ │ - AID 路由 │ ││ │ - GATT Client │ │ └─────────────────────────────┘ ││ │ - 广播/扫描 │ │ ││ └───────────────┘ │ │└─────────────────────┴──────────────────────────────────┘核心模块路径-ohos.bluetooth.ble— BLE 广播/扫描/GATT-ohos.bluetooth.connection— 经典蓝牙连接管理-ohos.nfc.tag— NFC 标签读写-ohos.nfc.cardEmulation— HCE 卡模拟---二、权限申请必须先搞定否则一切白搭2.1 所需权限清单// module.json5requestPermissions: [{ name: ohos.permission.USE_BLUETOOTH },{ name: ohos.permission.DISCOVER_BLUETOOTH },{ name: ohos.permission.MANAGE_BLUETOOTH },{ name: ohos.permission.LOCATION },{ name: ohos.permission.APPROXIMATELY_LOCATION },{ name: ohos.permission.NFC_TAG },{ name: ohos.permission.NFC_CARD_EMULATION }]2.2 动态申请BLE 扫描场景import { abilityAccessCtrl } from kit.AbilityKit;async function requestBlePermissions(context: Context): Promise {const atManager abilityAccessCtrl.createAtManager();const permissions [ohos.permission.LOCATION,ohos.permission.APPROXIMATELY_LOCATION,ohos.permission.USE_BLUETOOTH,];const result await atManager.requestPermissionsFromUser(context, permissions);// result.authResults[i] 0 表示已授权return result.authResults.every(r r 0);}坑点BLE 扫描在 API 12 上依然需要位置权限LOCATION或APPROXIMATELY_LOCATION缺少任意一个将导致startBLEScan静默失败扫描结果永远为空且不抛异常。---三、BLE 开发实战3.1 BLE 扫描流程调用 startBLEScan()│▼on(BLEDeviceFind) 持续回调│├─ 过滤 ServiceUUID / 设备名 / RSSI│▼stopBLEScan() ← 找到目标设备后立即停止节省电量│▼createGattClientDevice(deviceId)完整扫描代码import ble from ohos.bluetooth.ble;import { BusinessError } from ohos.base;const TARGET_SERVICE_UUID 0000FFF0-0000-1000-8000-00805F9B34FB;let gattClient: ble.GattClientDevice | null null;function startScan(): void {// 注册发现回调必须在 startBLEScan 之前注册ble.on(BLEDeviceFind, (devices: Array ) {for (const device of devices) {const hasTargetService device.serviceUuids?.includes(TARGET_SERVICE_UUID);if (hasTargetService) {ble.stopBLEScan(); // 立即停止避免持续扫描耗电connectDevice(device.deviceId);break;}}});const scanFilters: Array [{ serviceUuid: TARGET_SERVICE_UUID }];const scanOptions: ble.ScanOptions {interval: 0,dutyMode: ble.ScanDuty.SCAN_MODE_LOW_LATENCY,matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE,};try {ble.startBLEScan(scanFilters, scanOptions);} catch (e) {const err e as BusinessError;console.error(startBLEScan failed: ${err.code} ${err.message});}}3.2 GATT 连接与特征读写function connectDevice(deviceId: string): void {gattClient ble.createGattClientDevice(deviceId);gattClient.on(BLEConnectionStateChange, (state: ble.BLEConnectionChangeState) {if (state.state ble.ProfileConnectionState.STATE_CONNECTED) {// 连接成功后先发现服务否则直接读写会报错gattClient!.getServices().then((services: Array ) {const targetService services.find(s s.serviceUuid TARGET_SERVICE_UUID);const targetChar targetService?.characteristics?.find(c c.characteristicUuid 0000FFF1-0000-1000-8000-00805F9B34FB);if (targetChar) {gattClient!.readCharacteristicValue(targetChar).then(result {const value new Uint8Array(result.characteristicValue);console.info(Read: ${Array.from(value).map(b b.toString(16)).join( )});});}});}});gattClient.connect();}3.3 开启 BLE 通知Notifyasync function enableNotify(char: ble.BLECharacteristic): Promise {// 步骤1告知系统订阅通知await gattClient!.setNotifyCharacteristicChanged(char, true);// 步骤2向 CCCD 描述符写入 0x0100外围设备才会真正推送const cccd: ble.BLEDescriptor {serviceUuid: TARGET_SERVICE_UUID,characteristicUuid: char.characteristicUuid,descriptorUuid: 00002902-0000-1000-8000-00805F9B34FB,descriptorValue: new Uint8Array([0x01, 0x00]).buffer,};await gattClient!.writeDescriptorValue(cccd);// 步骤3注册数据变化回调gattClient!.on(BLECharacteristicChange, (charChange: ble.BLECharacteristic) {const data new Uint8Array(charChange.characteristicValue);console.info(Notify data: ${Array.from(data).map(b b.toString(16)).join( )});});}3.4 GATT Server外围设备模式let gattServer: ble.GattServer;function startGattServer(): void {gattServer ble.createGattServer();const service: ble.GattService {serviceUuid: TARGET_SERVICE_UUID,isPrimary: true,characteristics: [{serviceUuid: TARGET_SERVICE_UUID,characteristicUuid: 0000FFF1-0000-1000-8000-00805F9B34FB,characteristicValue: new ArrayBuffer(0),descriptors: [],properties: 0x0A, // READ | WRITE}],includeServices: [],};gattServer.addService(service);gattServer.on(characteristicWrite, (req: ble.CharacteristicWriteRequest) {const data new Uint8Array(req.value);console.info(Received: ${Array.from(data).map(b b.toString(16)).join( )});gattServer.sendResponse({deviceId: req.deviceId,transId: req.transId,status: 0,offset: 0,value: new ArrayBuffer(0),});});// 开始广播ble.startAdvertising({ interval: 160, txPower: 0, connectable: true },{ serviceUuids: [TARGET_SERVICE_UUID], manufactureData: [], serviceData: [] });}---四、NFC 标签读写实战4.1 NFC 标签分发机制设备靠近 NFC 标签│▼NFC 服务识别标签类型│├─ NDEF 标签 → action: ohos.nfc.action.TAG_DISCOVERED└─ ISO-DEP → action: ohos.nfc.action.TAG_DISCOVERED│▼匹配 module.json5 中的 skills.actions 配置│▼冷启动 → onCreate(want) / 热启动 → onNewWant(want)module.json5 配置两处都要处理skills: [{ actions: [ohos.nfc.action.TAG_DISCOVERED], uris: [] }]// UIAbility 中同时处理冷启动和热启动onCreate(want: Want): void { handleNfcWant(want); }onNewWant(want: Want): void { handleNfcWant(want); }function handleNfcWant(want: Want): void {const tagInfo tag.getTagInfo(want);if (tagInfo.technology.includes(tag.NfcTechnology.NDEF)) {readNdefTag(tagInfo);}}4.2 读取 NDEF 标签import tag from ohos.nfc.tag;async function readNdefTag(tagInfo: tag.TagInfo): Promise {const ndefTag tag.getNdef(tagInfo);await ndefTag.connect(); // 必须先 connecttry {const ndefMessage await ndefTag.readNdef();for (const record of ndefMessage.ndefRecords) {if (record.tnf tag.NfcTnfType.TNF_WELL_KNOWN) {const payload new Uint8Array(record.payload);// Text Record: 第一字节为状态字节后面是语言代码文本const langLen payload[0] 0x3F;const text new TextDecoder().decode(payload.slice(1 langLen));console.info(NDEF text: ${text});}}} finally {await ndefTag.close(); // 必须 close否则标签持续占用}}4.3 写入 NDEF 标签async function writeNdefTag(tagInfo: tag.TagInfo, message: string): Promise {const ndefTag tag.getNdef(tagInfo);await ndefTag.connect();try {const encoder new TextEncoder();const langCode zh;const langBytes encoder.encode(langCode);const textBytes encoder.encode(message);const statusByte langCode.length 0x3F;const payload new Uint8Array(1 langBytes.length textBytes.length);payload[0] statusByte;payload.set(langBytes, 1);payload.set(textBytes, 1 langBytes.length);const ndefMessage: tag.NdefMessage {ndefRecords: [{tnf: tag.NfcTnfType.TNF_WELL_KNOWN,rtdType: new Uint8Array([0x54]).buffer, // T Text RTDid: new ArrayBuffer(0),payload: payload.buffer,}]};await ndefTag.writeNdef(ndefMessage);} finally {await ndefTag.close();}}---五、HCE 卡模拟实战HCEHost Card Emulation让手机模拟为 NFC 卡片典型场景为门禁卡/交通卡。import cardEmulation from ohos.nfc.cardEmulation;class MyHceService extends cardEmulation.HceService {onCommand(apduData: number[]): void {console.info(APDU: ${apduData.map(b b.toString(16)).join( )});if (apduData[1] 0xA4) {this.sendResponse([0x90, 0x00]); // SELECT AID 成功} else {this.sendResponse([0x6D, 0x00]); // 不支持的指令}}}AID 注册resources/base/profile/hce_service.json{ aids: [A000000003000000] }---六、错误写法 → 问题 → 正确写法案例 1BLE 扫描无结果// ❌ 错误先 startBLEScan再注册回调ble.startBLEScan([], {});ble.on(BLEDeviceFind, (devices) { ... });// 问题回调注册晚于扫描启动早期扫描结果丢失// ✅ 正确先注册回调再启动扫描ble.on(BLEDeviceFind, (devices) { ... });ble.startBLEScan([], {});案例 2未发现服务直接读特征// ❌ 错误连接后直接读特征报错services not discoveredgattClient.on(BLEConnectionStateChange, (state) {if (state.state ProfileConnectionState.STATE_CONNECTED) {gattClient.readCharacteristicValue(char);}});// ✅ 正确连接后先 getServices从返回的 services 中取特征对象gattClient.on(BLEConnectionStateChange, (state) {if (state.state ProfileConnectionState.STATE_CONNECTED) {gattClient.getServices().then(services {const char services[0].characteristics[0];gattClient.readCharacteristicValue(char);});}});案例 3NFC 标签未关闭连接// ❌ 错误用完不 close → 标签持续被占用const ndefTag tag.getNdef(tagInfo);await ndefTag.connect();await ndefTag.readNdef();// ✅ 正确try/finally 确保 closeawait ndefTag.connect();try {await ndefTag.readNdef();} finally {await ndefTag.close();}---七、最佳实践7.1 BLE 扫描找到目标立即停止做法找到目标设备后立即调用ble.stopBLEScan()。原因SCAN_MODE_LOW_LATENCY模式下持续扫描功耗约是 idle 的 5~10 倍。不这样做会怎样耗电明显增加部分系统还会在后台强制降级扫描频率。7.2 GATT 连接复用按指数退避重连做法缓存GattClientDevice实例监听连接状态断开后以 1s → 2s → 4s 间隔重连。原因每次createGattClientDevice connect需要完整 BLE 握手通常耗时 500ms~2s。不这样做会怎样用户每次打开页面都经历秒级等待高频重连可能触发对端设备防抖被拒绝连接。7.3 NFC 操作异步执行勿阻塞主线程做法将connect/read/write/close通过async/await TaskPool 与 UI 线程隔离。原因NFC I/O 存在不确定延迟10~200ms主线程阻塞导致帧率抖动甚至 ANR。不这样做会怎样UI 卡顿严重时系统弹出应用未响应对话框。7.4 监听蓝牙状态就绪后再初始化做法通过bluetooth.on(stateChange, ...)监听开关在STATE_ON后再启动扫描/广播。原因蓝牙开关切换是异步过程立即调用 API 会抛2900099BT_ERR_INTERNAL_ERROR。不这样做会怎样应用启动时若蓝牙未就绪初始化必然失败错误信息不直观排查困难。---八、核心坑点坑点 1BLE 扫描需要位置权限但静默失败现象startBLEScan调用后BLEDeviceFind回调始终为空无任何报错日志。原因API 12 中 BLE 扫描依赖位置能力缺少LOCATION或APPROXIMATELY_LOCATION时系统静默忽略扫描结果。复现移除module.json5中位置权限后运行扫描观察BLEDeviceFind从不触发。解决在startBLEScan前先申请位置权限确认授权后再启动扫描。---坑点 2GATT 通知未写 CCCD 描述符现象readCharacteristicValue正常但on(BLECharacteristicChange)回调永远不触发。原因BLE Notify 要求客户端向 CCCDUUID:00002902-...写入0x0100开启通知缺少此步骤外围设备不会主动推送数据。复现只调用setNotifyCharacteristicChanged(char, true)不写 CCCD 描述符监听无效。解决setNotifyCharacteristicChangedwriteDescriptorValue(CCCD, [0x01, 0x00])必须同时执行见第三节示例。---坑点 3NFC onNewWant 中 tagInfo 为 undefined现象应用被 NFC 拉起后tag.getTagInfo(want)返回 undefined 或抛异常。原因冷启动时标签信息在onCreate的want中热启动时在onNewWant中——只处理其中一个导致特定场景 tagInfo 为空。复现只在onNewWant处理冷启动靠近标签时tagInfo为空。解决onCreate和onNewWant共用同一处理函数两处都调用tag.getTagInfo(want)。---九、总结1.权限是基础BLE 扫描需位置权限NFC 需NFC_TAG/NFC_CARD_EMULATION缺一且静默失败。2.BLE 流程有序先注册回调 → 再启动扫描连接后先getServices→ 再操作特征。3.NFC 资源必须释放connect后必须closetry/finally是标准写法。4.GATT 通知需双写setNotifyCharacteristicChanged CCCD 描述符写入缺一不可。5.核心结论BLE 和 NFC 开发最大陷阱在于静默失败——无报错无日志必须通过 hilog 过滤nfc/bluetoothtag 定位根因。---参考资料- HarmonyOS 官方文档 · 蓝牙 BLE 开发指南- HarmonyOS 官方文档 · NFC 标签读写- HarmonyOS 官方文档 · HCE 卡模拟- OpenHarmony 蓝牙源码foundation/communication/bluetooth/frameworks/inner/src/bluetooth_ble_central_manager.cpp- OpenHarmony NFC 源码foundation/communication/nfc/services/src/nfc_service.cpp

相关新闻