手机贴一贴就能开门?——鸿蒙蓝牙 & NFC 开发的“又快又稳”实战手册!
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
先抛个直球:要不是蓝牙怕你不连、NFC怕你不对齐,移动端硬件开发早就一路小跑了。好消息是,鸿蒙(HarmonyOS / OpenHarmony)在 ArkUI + ArkTS 下已经把这俩“社恐硬件”照顾得很体贴:API 越来越顺,系统权限与能力声明也更清晰。
这篇不走“百科背诵”路线,我会用工程师的焦虑点来组织:蓝牙通信框架 API 怎么落地?NFC 标签读写到底哪几步?权限、兼容、功耗、稳定性怎么兜底?最后再给两套能跑的实际开发案例(BLE 外设读写 / NFC NDEF 名片 & 门禁小卡),让你合起文档就能开工。开整!🚀
目录导航
- 项目基线:能力声明、权限与工程骨架
- 蓝牙通信框架:BLE 扫描/连接/GATT/通知/MTU
- NFC 标签读写:NDEF 读写、格式化与防踩坑
- 实际开发案例 A:BLE 设备仪表盘(手机↔传感器)
- 实际开发案例 B:NFC 智能名片/门禁小卡
- 稳定性与 QoS:功耗、自动重连、权限与兼容矩阵
- 调试与避坑清单
- 收尾:把“贴一贴、连一连”做成可靠体验
项目基线:能力声明、权限与工程骨架
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(UUID0xF101,Notify)、HUMI(0xF102,Notify)、CONF(0xF103,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/vcardMIME 记录; - 门禁场景多涉及安全域/加密扇区,通常不等同 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 !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)