手机贴一贴就能开门?——鸿蒙蓝牙 & NFC 开发的“又快又稳”实战手册!

举报
喵手 发表于 2025/10/31 17:47:56 2025/10/31
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

先抛个直球:要不是蓝牙怕你不连、NFC怕你不对齐,移动端硬件开发早就一路小跑了。好消息是,鸿蒙(HarmonyOS / OpenHarmony)在 ArkUI + ArkTS 下已经把这俩“社恐硬件”照顾得很体贴:API 越来越顺,系统权限与能力声明也更清晰。
  这篇不走“百科背诵”路线,我会用
工程师的焦虑点
来组织:蓝牙通信框架 API 怎么落地?NFC 标签读写到底哪几步?权限、兼容、功耗、稳定性怎么兜底?最后再给两套能跑的实际开发案例(BLE 外设读写 / NFC NDEF 名片 & 门禁小卡),让你合起文档就能开工。开整!🚀

目录导航

  1. 项目基线:能力声明、权限与工程骨架
  2. 蓝牙通信框架:BLE 扫描/连接/GATT/通知/MTU
  3. NFC 标签读写:NDEF 读写、格式化与防踩坑
  4. 实际开发案例 A:BLE 设备仪表盘(手机↔传感器)
  5. 实际开发案例 B:NFC 智能名片/门禁小卡
  6. 稳定性与 QoS:功耗、自动重连、权限与兼容矩阵
  7. 调试与避坑清单
  8. 收尾:把“贴一贴、连一连”做成可靠体验

项目基线:能力声明、权限与工程骨架

1) 能力与权限(module.json5 / AppScope/app.json5

不声明就拿不到硬件,第一步永远是权限与设备能力

// module.json5(片段)
{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      { "name": "ohos.permission.DISCOVER_BLUETOOTH" },
      { "name": "ohos.permission.MANAGE_BLUETOOTH" },
      { "name": "ohos.permission.CONNECT_BLUETOOTH" },
      { "name": "ohos.permission.GET_NETWORK_INFO" },
      { "name": "ohos.permission.NFC_TAG" }
    ],
    "deviceTypes": [ "phone", "tablet", "wearable" ],
    "abilities": [
      {
        "name": "EntryAbility",
        "exported": true,
        "skills": [{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }]
      }
    ],
    "metadata": [
      { "name": "req_capability.bluetooth", "value": "true" },
      { "name": "req_capability.nfc", "value": "true" }
    ]
  }
}

小叮嘱:不同 API Level 的权限命名可能有微调;真机调试时若出现PERMISSION_DENIED,先看系统设置-权限里是否启用“附近设备/蓝牙/NFC”。

2) 结构建议(Stage 模型)

/entry
  /src/main/ets
    /app            // App级初始化(主题、依赖注入)
    /ability        // EntryAbility
    /pages          // UI 页面
    /service        // 蓝牙&NFC服务封装
      bluetooth/
      nfc/
    /components     // 复用UI
  /resources
  /module.json5
/appscope/app.json5

蓝牙通信框架:BLE 扫描/连接/GATT/通知/MTU

本节围绕 BLE(Bluetooth Low Energy) 的典型闭环:扫描 → 过滤 → 连接 → 服务发现 → 读/写 → 通知/指示 → MTU 协商 → 断开/重连
说明:以下示例基于 ArkTS 语法 & 常见蓝牙命名;不同版本 API 名称略有差异(例如 @ohos.bluetooth.*@ohos.bluetooth.ble/*),你可以按工程实际 import。

1) 扫描与过滤

// service/bluetooth/bleScanner.ets
import ble from '@ohos.bluetooth.ble'; // 示例命名

export class BleScanner {
  private scanning = false;

  startScan(filters?: { serviceUuids?: string[] }) {
    if (this.scanning) return;
    this.scanning = true;

    ble.startBLEScan({
      interval: 1000,
      reportDelayMillis: 0,
      filters: filters ? [{ serviceUuid: filters.serviceUuids?.[0] }] : []
    }, (result) => {
      // result:包含 deviceId、rssi、advertData 等
      console.info('[BLE] found:', result?.deviceId, result?.rssi);
    });
  }

  stopScan() {
    if (!this.scanning) return;
    ble.stopBLEScan();
    this.scanning = false;
  }
}

Tips

  • 扫描别常开,进入目标页面再开,离开即停;
  • 过滤 serviceUuid 可显著降低无关广播对 UI 的冲击;
  • 某些固件广播包只带短 UUID,要结合名字前缀/RSSI辅助过滤。

2) 连接 & 服务发现

// service/bluetooth/bleClient.ets
import ble from '@ohos.bluetooth.ble';

export class BleClient {
  private gatt?: any;

  async connect(deviceId: string): Promise<void> {
    this.gatt = await ble.createGattClient(deviceId);
    await this.gatt.connect();            // 连接
    await this.gatt.discoverServices();   // 服务发现
  }

  async read(service: string, char: string): Promise<Uint8Array> {
    const v = await this.gatt.readCharacteristicValue({ serviceUuid: service, characteristicUuid: char });
    return new Uint8Array(v?.value || []);
  }

  async write(service: string, char: string, data: Uint8Array, withResponse = true) {
    await this.gatt.writeCharacteristicValue({
      serviceUuid: service, characteristicUuid: char, value: data, writeType: withResponse ? 1 : 2
    });
  }

  async enableNotify(service: string, char: string, cb: (v: Uint8Array) => void) {
    await this.gatt.setCharacteristicChangedCallback(({ value, serviceUuid, characteristicUuid }) => {
      if (serviceUuid === service && characteristicUuid === char) cb(new Uint8Array(value));
    });
    await this.gatt.subscribeCharacteristic({
      serviceUuid: service, characteristicUuid: char, enable: true
    });
  }

  async requestMtu(size = 185) {
    try { await this.gatt.requestMtu(size); } catch (_) {}
  }

  async disconnect() {
    try { await this.gatt?.disconnect(); } finally { this.gatt = undefined; }
  }
}

Tips

  • MTU:iOS 常见 185、安卓常见 517(取决于栈 & 控制器);协商不成就回落
  • 写入时区分 Write With Response(稳定)/ Without Response(快但可能丢);
  • 通知回调里避免重逻辑,丢给消息队列或状态机。

3) 配对/加密与重连

  • 若服务端要求加密,先触发配对流程
  • 记录 deviceId + bondState,APP 冷启动后优先尝试直连
  • 建议实现指数退避的重连策略:2s / 4s / 8s …(上限 30–60s)。

NFC 标签读写:NDEF 读写、格式化与防踩坑

NFC 在移动端最稳的落地NDEF(NFC Data Exchange Format):URI、文本、名片(vCard)、自定义 MIME 等。

1) 会话与发现回调

// service/nfc/nfcService.ets
import nfc from '@ohos.nfc.tag'; // 示例命名,实际按SDK导入

export class NfcService {
  private session?: any;

  startSession(onTag: (techs: string[], handle: number) => void) {
    this.session = nfc.createTagSession({
      onTagDiscovered: (tagInfo) => {
        // 包含 tech 列表与 handle(会话句柄)
        onTag(tagInfo?.techList || [], tagInfo?.tagHandle);
      },
      onTagLost: () => console.info('[NFC] tag lost')
    });
    this.session?.start();
  }

  stopSession() { this.session?.stop(); this.session = undefined; }
}

2) 读取 NDEF

// service/nfc/ndef.ets
import ndef from '@ohos.nfc.ndef';

export async function readNdef(tagHandle: number) {
  const proxy = await ndef.getNdef(tagHandle);
  const message = await proxy.readNdef();
  // message.records: [{tnf, type, id, payload}]
  return message?.records?.map((r: any) => decodeRecord(r)) || [];
}

function decodeRecord(r: any) {
  // 简化示意:Text记录
  if (r.tnf === 0x01 && String.fromCharCode(...r.type) === 'T') {
    const langLen = r.payload[0] & 0x1F;
    const text = new TextDecoder().decode(r.payload.slice(1 + langLen));
    return { type: 'text', text };
  }
  // URI/MIME/自定义... 此处省略
  return { raw: r };
}

3) 写入 NDEF

export async function writeText(tagHandle: number, text: string) {
  const encoder = new TextEncoder();
  const lang = encoder.encode('en');
  const payload = new Uint8Array(1 + lang.length + encoder.encode(text).length);
  payload[0] = lang.length; // status byte
  payload.set(lang, 1);
  payload.set(encoder.encode(text), 1 + lang.length);

  const record = {
    tnf: 0x01,                          // Well-known
    type: new TextEncoder().encode('T'),// Text
    id: new Uint8Array([]),
    payload
  };
  const msg = { records: [record] };

  const proxy = await ndef.getNdef(tagHandle);
  // 如果标签未格式化为 NDEF,可尝试 format(部分卡支持)
  if (!(await proxy.isNdefWritable())) throw new Error('Tag not writable');
  await proxy.writeNdef(msg);
}

常见卡类型与要点

  • NFC Forum NDEF 标签:最省心(Type 2/4 常见);
  • MIFARE Classic:部分设备不原生支持,注意兼容;
  • 写保护:一旦 makeReadOnly(),就不可逆,谨慎上锁;
  • 容量:写入前先 getNdefTagType() / getMaxSize() 估容量。

实际开发案例 A:BLE 设备仪表盘(手机↔传感器)

需求:手机连接 BLE 温湿度传感器,1s 更新一次;设备特征:

  • Service UUID:0xF00D
  • Characteristic:TEMP(UUID 0xF101,Notify)、HUMI0xF102,Notify)、CONF0xF103,RW)

UI:实时卡片

// pages/DeviceDashboard.ets
import { BleScanner } from '../service/bluetooth/bleScanner';
import { BleClient } from '../service/bluetooth/bleClient';

@Entry
@Component
export struct DeviceDashboard {
  private scanner = new BleScanner();
  private client = new BleClient();
  @State temp: string = '--';
  @State humi: string = '--';
  @State connected = false;

  aboutToAppear() { this.scanner.startScan({ serviceUuids: ['0000F00D-0000-1000-8000-00805F9B34FB'] }); }
  aboutToDisappear() { this.scanner.stopScan(); this.client.disconnect(); }

  async connect(deviceId: string) {
    await this.client.connect(deviceId);
    await this.client.requestMtu(185);
    await this.client.enableNotify('F00D', 'F101', v => this.temp = parseTemp(v));
    await this.client.enableNotify('F00D', 'F102', v => this.humi = parseHumi(v));
    this.connected = true;
  }

  build() {
    Column({ space: 12 }) {
      Text(`Temperature: ${this.temp} °C`).fontSize(22)
      Text(`Humidity: ${this.humi} %`).fontSize(22)
      Row({ space: 8 }) {
        Button(this.connected ? 'Disconnect' : 'Connect')
          .onClick(async () => this.connected ? await this.client.disconnect() : await this.connect('<device-id>'))
        Button('Set Interval=1s')
          .onClick(async () => await this.client.write('F00D','F103',new Uint8Array([0x01])))
      }
    }.padding(16)
  }
}

function parseTemp(v: Uint8Array) { return (DataViewFrom(v).getInt16(0, true) / 100).toFixed(2); }
function parseHumi(v: Uint8Array) { return (DataViewFrom(v).getUint16(0, true) / 100).toFixed(2); }
function DataViewFrom(v: Uint8Array) { return new DataView(v.buffer, v.byteOffset, v.byteLength); }

工程要点

  • Notify 的频率由固件控制;上层节流避免 UI 过载;
  • CONF 写入采用 With Response,保证下发可靠;
  • 加入断线重连:监听 onConnectionStateChange,指数退避。

实际开发案例 B:NFC 智能名片/门禁小卡

目标

  • 模式 1:把个人名片写成 NDEF Text/URI,别人一贴立即打开个人主页;
  • 模式 2:对接公司门禁(前提:公司系统支持自定义 NDEF 或内部密钥卡模拟,需合规)。

智能名片写入

// pages/NfcCard.ets
import { NfcService } from '../service/nfc/nfcService';
import { writeText, readNdef } from '../service/nfc/ndef';

@Entry
@Component
export class NfcCard {
  private nfc = new NfcService();
  @State last: string = '—';

  aboutToAppear() {
    this.nfc.startSession(async (techs, handle) => {
      try {
        await writeText(handle, 'https://example.com/u/yourname');
        this.last = '写入成功:个人主页链接';
      } catch (e) {
        this.last = `写入失败:${(e as Error).message}`;
      }
    });
  }

  aboutToDisappear() { this.nfc.stopSession(); }

  build() {
    Column({ space: 8 }) {
      Text('把卡片/手机背面靠近标签…').fontSize(18)
      Text(this.last).fontColor('#999')
    }.padding(16)
  }
}

要点

  • 若要写vCard,可写入 text/vcard MIME 记录;
  • 门禁场景多涉及安全域/加密扇区,通常不等同 NDEF,需与门禁厂商/安保部门对接(合规第一)。

稳定性与 QoS:功耗、自动重连、权限与兼容矩阵

1) BLE 功耗守则

  • 扫描分场景:前台页面开、后台即停;
  • 连接保持:无数据时降低通知频度或进入外设低功耗模式
  • MTU/PHY:2M PHY(若硬件支持)可降空口占用;MTU 合理即可,别执念。

2) 自动重连与状态机

IDLE -> SCANNING -> CONNECTING -> DISCOVERING -> ACTIVE
ACTIVE --link lost--> RETRY(n) -> (backoff) -> SCANNING
  • 每个跃迁埋点:耗时、失败码;
  • RETRY 3 次以上且短时间失败:提示用户靠近/重启外设

3) 兼容矩阵(建议)

维度 最小/推荐 备注
HarmonyOS / OpenHarmony API Level 对齐项目基线 关注蓝牙/NFC命名差异
设备类型 Phone / Tablet / Wearable 可选分支:手表权限弹窗不同
BLE 控制器 4.2 / 5.0+ 5.0 支持 2M PHY,更省时延
NFC 标签 Type 2/4 容量 & 只读状态预检
厂商差异 华为/荣耀/… 真机回归:扫描/配对/配网

调试与避坑清单

蓝牙

  • [ ] 扫描去抖:合并相同 deviceId 的结果,UI 列表别闪烁
  • [ ] 服务发现后再读写,别“裸写”
  • [ ] 通知黏包/分包:应用层做 TLV 或帧头长度字段
  • [ ] Write Without Response流控(计时/计数/ACK)
  • [ ] 权限二段式:首次允许、系统设置页回退路径

NFC

  • [ ] 写前 isNdefWritable() + 容量检查
  • [ ] Text/URI 编码:UTF-8 + 正确 status byte/lang code
  • [ ] 防止写一半:写入后重读校验
  • [ ] 贴合识别区:不同机型线圈位置不同,给 UI 提示
  • [ ] 只读不可逆:上线前评审

通用

  • [ ] 真机冷/热启动切换,权限丢失处理
  • [ ] 后台/锁屏行为与连接策略
  • [ ] 失败码归一化:面向用户的友好提示建议动作
  • [ ] 埋点:连接成功率、重连次数、NFC 成功率、均值耗时

收尾:把“贴一贴、连一连”做成可靠体验

总结一句话:蓝牙是“稳定握手 + 节制传输”,NFC是“一击即中 + 写后必验”。只要你把能力声明、权限、状态机、功耗、容错这几件事按本文的清单走一遍,用户就只会觉得:“哇,贴一下就好了,连一下就行了。”
  最后留个小反问:**你想要的是“偶尔灵光一现”的连接,还是“每次都靠谱”的体验?**如果是后者,这份手册就当你的默认 Playbook 吧。😉


附:快速复用的常量与工具

// service/bluetooth/constants.ets
export const UUID = {
  SERVICE_ENV: '0000F00D-0000-1000-8000-00805F9B34FB',
  CHAR_TEMP:   '0000F101-0000-1000-8000-00805F9B34FB',
  CHAR_HUMI:   '0000F102-0000-1000-8000-00805F9B34FB',
  CHAR_CONF:   '0000F103-0000-1000-8000-00805F9B34FB'
};

export function hex(buf: Uint8Array) {
  return [...buf].map(b => b.toString(16).padStart(2,'0')).join('');
}
// service/nfc/records.ets — 构建常见 NDEF 记录
export function ndefText(text: string, lang = 'en') {
  const enc = new TextEncoder();
  const l = enc.encode(lang);
  const t = enc.encode(text);
  const payload = new Uint8Array(1 + l.length + t.length);
  payload[0] = l.length;
  payload.set(l, 1);
  payload.set(t, 1 + l.length);
  return { tnf: 0x01, type: enc.encode('T'), id: new Uint8Array([]), payload };
}

export function ndefUri(uri: string) {
  const enc = new TextEncoder();
  const u = enc.encode(uri);
  const payload = new Uint8Array(1 + u.length);
  payload[0] = 0x00; // 无缩写
  payload.set(u, 1);
  return { tnf: 0x01, type: enc.encode('U'), id: new Uint8Array([]), payload };
}

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。