第8章 8.3 蓝牙与 NFC
🎯 开场:你的手机为什么能刷公交卡?
你有没有想过一个问题——
公交卡明明没电池、没充电,怎么往里面充钱的?地铁闸机也没联网,怎么知道你这张卡里有没有钱?
因为有一种技术叫 NFC(近场通信),它不需要电,只需要一个「感应」就能传递信息。就像两个人击掌,不用说话、不用微信,碰一下就传递了信号。
这还没完——
你用手机蓝牙开过智能门锁吗?手机明明没连网,怎么就「滴」一声开了门?这就是蓝牙的力量,它不需要 WiFi,不需要互联网,靠自己就能两个设备之间「聊悄悄话」。
痛点来了:你以为这些功能很复杂,手机 App 开发者才能做?错!uniapp 早就把这些能力封装好了,几行代码你就能调用。
这一章,我们用 Python 的思维(对,你没看错,用写 Python 的逻辑)来理解 uniapp 的蓝牙和 NFC 开发。
学完本文你能做到:
- 用代码搜索附近的蓝牙设备
- 连接蓝牙设备并读写数据
- 实现一个 NFC 模拟器(模拟刷门禁卡)
- 做一个「蓝牙控制灯」的小工具
\n\n
\n\n
\n\n---
🧱 基础:蓝牙与 NFC 到底是什么?
8.3.1 蓝牙:设备间的「悄悄话」
生活类比:蓝牙就像两个人用手势交流——不需要手机信号,不需要网络,但离得太远(超过 10 米)就听不见了。而且一个主设备只能同时和 7 个人「聊」(蓝牙协议限制)。
在uniapp里,蓝牙分两种:
| 类型 | 解释 | 举个例子 |
|---|---|---|
| 主设备 | 主动发起连接的那一方 | 你的手机 |
| 从设备 | 等待被连接的那一方 | 蓝牙音箱、手环、智能门锁 |
为什么用蓝牙而不是 WiFi?
- 蓝牙耗电超低(一颗纽扣电池能用几年)
- 连接速度快(毫秒级)
- 适合「一直连接着」的场景(智能家居)
8.3.2 NFC:不用电的「击掌」通信
生活类比:NFC 就像小时候玩的「拍手传话」游戏——两个人手掌对手掌,一拍就把信息传过去了。但距离更近,必须 4厘米以内 才能通信。
NFC 有三种模式(你只需要记住一种就行):
| 模式 | 能干啥 | 你的场景 |
|---|---|---|
| 读写模式 | 读取 NFC 标签里的数据 | 刷公交卡、读取海报 |
| 卡模拟模式 | 把手机模拟成一张卡 | 模拟门禁卡 |
| 点对点模式 | 两个手机碰一碰传文件 | 碰一碰分享(微信有这个功能) |
坑来了:很多人以为 NFC 和蓝牙是一回事,错!蓝牙传数据多、距离远(10米+),但 NFC 不用电、碰一下就行。你刷公交卡如果用蓝牙,早高峰排队你能急死。
8.3.3 uniapp 蓝牙开发三步走
在 uniapp 里玩蓝牙,总共分三步:
第1步:打开蓝牙适配器(初始化)
第2步:搜索周围设备
第3步:连接设备并通信
听起来简单,但每一步都有「坑」,我们一个个说。
8.3.4 代码时间:你的第一个蓝牙程序
目标:搜索附近所有蓝牙设备,把名字打印出来。
# -*- coding: utf-8 -*-
# 注意:这是 uniapp 的 JavaScript 代码,但用 Python 风格写注释帮你理解逻辑
# uniapp 蓝牙三步曲
# 第1步:初始化蓝牙适配器
def init_bluetooth():
"""
相当于打开蓝牙开关。
类比:你要用微信,得先打开手机蓝牙。
"""
# 伪代码,实际是 JS:
# uni.openBluetoothAdapter()
pass
# 第2步:开始搜索设备
def search_devices():
"""
搜索周围的蓝牙设备。
类比:在房间里喊一声「有人在吗」,周围的人会应答。
"""
# 伪代码,实际是 JS:
# uni.startBluetoothDevicesDiscovery()
pass
# 第3步:获取已发现的设备列表
def get_device_list():
"""
把搜索到的设备名字拿出来。
类比:把刚才应答的人都记到小本本上。
"""
# 伪代码,实际是 JS:
# uni.getBluetoothDevices()
pass
实际可运行的 uniapp 代码(JavaScript):
// 第1步:打开蓝牙适配器
uni.openBluetoothAdapter({
success: (res) => {
console.log("蓝牙适配器打开成功", res);
// 成功后立刻搜索
startDiscovery();
},
fail: (err) => {
console.error("蓝牙打开失败", err);
uni.showToast({ title: "请检查手机蓝牙是否开启" });
}
});
// 第2步:搜索设备
function startDiscovery() {
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false, // 是否重复上报同一设备
success: (res) => {
console.log("开始搜索蓝牙设备...");
}
});
}
// 第3步:监听发现新设备
uni.onBluetoothDeviceFound((res) => {
const devices = res.devices;
devices.forEach(device => {
console.log("发现设备:", device.name || "无名设备", device.deviceId);
});
});
// 获取已发现的设备列表
uni.getBluetoothDevices({
success: (res) => {
console.log("已发现的设备列表:", res.devices);
}
});
代码在干嘛:打开蓝牙 → 搜索周围设备 → 打印设备名。
8.3.5 NFC 开发:读一张公交卡
NFC 比蓝牙简单,因为它不需要「连接」,只需要「碰一下」。
目标:读取一张 NFC 标签的 ID。
// 定义一个读取 NFC 的函数
function readNFC() {
// 检查设备是否支持 NFC
const nfc = uni.getNFCAdapter();
if (!nfc) {
uni.showToast({ title: "该设备不支持NFC" });
return;
}
// 开始监听 NFC 标签
nfc.startDiscovery({
success: () => {
console.log("NFC 监听已开启,请碰一下标签");
},
fail: (err) => {
console.error("NFC 初始化失败", err);
}
});
// 当检测到 NFC 标签时触发
nfc.onDiscovered((res) => {
const tag = res.tag; // 标签信息
console.log("读到 NFC 标签:", tag.id); // 标签 ID(类似身份证号)
uni.showToast({ title: `读到标签: ${tag.id}` });
});
}
// 调用读取函数
readNFC();
代码在干嘛:开启 NFC 监听 → 等你拿标签碰手机 → 读到标签 ID 后弹窗显示。
🔥 实战:3 个项目带你从入门到跑通
📦 项目 1:蓝牙设备扫描器(5 分钟)
场景:你要做一个 App,扫描周围的蓝牙设备并在列表里显示。
完整代码:
// pages/bluetooth/scanner.js
Page({
data: {
devices: [], // 设备列表
scanning: false, // 是否正在扫描
},
// 打开蓝牙并开始扫描
startScan() {
this.setData({ scanning: true, devices: [] });
// 第1步:打开蓝牙适配器
uni.openBluetoothAdapter({
success: () => {
console.log("蓝牙适配器已打开");
this._startDiscovery();
},
fail: () => {
uni.showToast({ title: "请开启手机蓝牙" });
this.setData({ scanning: false });
}
});
},
// 第2步:开始搜索
_startDiscovery() {
uni.startBluetoothDevicesDiscovery({
success: () => {
console.log("正在扫描...");
}
});
// 第3步:监听新设备
uni.onBluetoothDeviceFound((res) => {
this._addDevice(res.devices[0]);
});
// 顺便把已发现的设备也拿过来
uni.getBluetoothDevices({
success: (res) => {
res.devices.forEach(d => this._addDevice(d));
}
});
},
// 添加设备到列表(去重)
_addDevice(device) {
const devices = this.data.devices;
const exists = devices.find(d => d.deviceId === device.deviceId);
if (!exists) {
this.setData({
devices: [...devices, {
name: device.name || "未知设备",
id: device.deviceId,
rssi: device.RSSI || "未知"
}]
});
}
},
// 点击设备查看详情
onDeviceTap(e) {
const device = e.currentTarget.dataset.device;
uni.showModal({
title: device.name,
content: `设备ID: ${device.id}\n信号强度: ${device.rssi}`
});
},
// 停止扫描
stopScan() {
uni.stopBluetoothDevicesDiscovery();
this.setData({ scanning: false });
},
// 页面卸载时关闭蓝牙
onUnload() {
this.stopScan();
uni.closeBluetoothAdapter();
}
});
对应的 wxml:
<!-- pages/bluetooth/scanner.wxml -->
<view class="container">
<button bindtap="startScan" disabled="{{scanning}}">
{{scanning ? '扫描中...' : '开始扫描'}}
</button>
<button bindtap="stopScan" wx:if="{{scanning}}">停止</button>
<view class="device-list">
<view wx:for="{{devices}}" wx:key="deviceId"
class="device-item"
bindtap="onDeviceTap"
data-device="{{item}}">
<text class="name">{{item.name}}</text>
<text class="id">ID: {{item.id}}</text>
<text class="rssi">信号: {{item.rssi}}</text>
</view>
</view>
</view>
预期输出:
[按钮] 开始扫描
[点击后显示设备列表]
- 小米手环7 ID: XX:XX:XX:... 信号: -67
- Bluetooth Speaker ID: YY:YY:YY:... 信号: -45
📦 项目 2:NFC 记账本(15 分钟)
场景:你有一叠 NFC 标签,每个标签代表一笔「待办记账」。比如标签 A 是「早餐 15 元」,标签 B 是「打车 30 元」。碰一下标签,手机就自动记录这笔账。
数据准备:先创建几个 NFC 标签,写入以下 JSON 数据:
{"type": "expense", "amount": 15, "category": "food", "desc": "早餐"}
{"type": "expense", "amount": 30, "category": "transport", "desc": "打车"}
{"type": "expense", "amount": 128, "category": "shopping", "desc": "图书"}
完整代码:
// pages/nfc/ledger.js
Page({
data: {
records: [], // 记账记录
total: 0, // 总支出
},
// 初始化 NFC
onLoad() {
this.initNFC();
},
// 初始化 NFC 适配器
initNFC() {
const nfc = uni.getNFCAdapter();
if (!nfc) {
uni.showToast({ title: "设备不支持NFC" });
return;
}
// 开始监听
nfc.startDiscovery({
success: () => {
console.log("NFC 已就绪,请碰标签");
uni.showToast({ title: "NFC 已就绪" });
}
});
// 读到标签时触发
nfc.onDiscovered((res) => {
this._handleTag(res.tag);
});
},
// 处理标签数据
_handleTag(tag) {
// 解析 NFC 标签里的数据
let data;
try {
// 标签数据在 tag.buffer 里(ArrayBuffer 转字符串)
const decoder = new TextDecoder('utf-8');
const jsonStr = decoder.decode(tag.buffer);
data = JSON.parse(jsonStr);
} catch (e) {
// 如果解析失败,说明标签里存的不是 JSON
data = { type: "unknown", desc: "未知标签" };
}
// 验证数据合法性
if (data.type !== "expense") {
uni.showToast({ title: "这不是记账标签" });
return;
}
// 记录这笔账
const records = this.data.records;
const newRecord = {
id: Date.now(),
amount: data.amount,
category: data.category,
desc: data.desc,
time: this._formatTime(new Date())
};
records.unshift(newRecord); // 最新记录在前面
const total = this.data.total + data.amount;
this.setData({
records,
total
});
// 顺便存到本地
uni.setStorageSync("expense_records", records);
uni.setStorageSync("expense_total", total);
uni.showToast({
title: `已记录: ${data.desc} ${data.amount}元`
});
},
// 格式化时间
_formatTime(date) {
return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
// 清空记录
clearRecords() {
uni.showModal({
title: "确认清空",
content: "确定要清空所有记录吗?",
success: (res) => {
if (res.confirm) {
this.setData({ records: [], total: 0 });
uni.removeStorageSync("expense_records");
uni.removeStorageSync("expense_total");
}
}
});
},
// 查看总支出
viewTotal() {
uni.showModal({
title: "本月支出",
content: `总计: ${this.data.total} 元`
});
}
});
预期输出:
碰一下「早餐」标签 → [已记录: 早餐 15元]
碰一下「打车」标签 → [已记录: 打车 30元]
碰一下「图书」标签 → [已记录: 图书 128元]
[查看总支出] → 总计: 173 元
代码在干嘛:碰 NFC 标签 → 解析 JSON 数据 → 更新记录列表和总金额 → 自动保存。
📦 项目 3:蓝牙温湿度监控器(15 分钟)
场景:公司有一个蓝牙温湿度传感器,每 5 秒上报一次数据。你要做一个 App 显示当前温度和湿度,超过阈值就报警。
项目结构:
pages/
└── sensor/
├── monitor.js # 主逻辑
├── monitor.wxml # 界面
└── monitor.wxss # 样式
完整代码(monitor.js):
// pages/sensor/monitor.js
Page({
data: {
connected: false, // 是否已连接
deviceId: null, // 设备ID
temperature: "--", // 温度
humidity: "--", // 湿度
alertLevel: 0, // 报警等级 0=正常 1=预警 2=危险
history: [], // 历史记录
},
// 连接到传感器
connectSensor(deviceId) {
uni.showLoading({ title: "连接中..." });
uni.createBLEConnection({
deviceId: deviceId,
success: (res) => {
console.log("连接成功", res);
this.setData({
connected: true,
deviceId: deviceId
});
uni.hideLoading();
// 获取服务列表
this._getServices(deviceId);
},
fail: (err) => {
console.error("连接失败", err);
uni.hideLoading();
uni.showToast({ title: "连接失败" });
}
});
},
// 获取蓝牙服务
_getServices(deviceId) {
uni.getBLEDeviceServices({
deviceId: deviceId,
success: (res) => {
console.log("服务列表", res.services);
// 假设第一个服务是我们要的
const service = res.services[0];
this._getCharacteristics(deviceId, service.uuid);
}
});
},
// 获取特征值(用于订阅数据)
_getCharacteristics(deviceId, serviceId) {
uni.getBLEDeviceCharacteristics({
deviceId: deviceId,
serviceId: serviceId,
success: (res) => {
console.log("特征列表", res.characteristics);
// 找可通知的特征(传感器会往这里推数据)
const char = res.characteristics.find(
c => c.properties.notify || c.properties.indicate
);
if (char) {
this._notifyCharacteristic(deviceId, serviceId, char.uuid);
}
}
});
},
// 订阅特征值变化(接收传感器数据)
_notifyCharacteristic(deviceId, serviceId, charId) {
uni.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: charId,
state: true,
success: () => {
console.log("订阅成功,开始接收数据");
this._startListen();
}
});
},
// 监听数据变化
_startListen() {
uni.onBLECharacteristicValueChange((res) => {
// 把 ArrayBuffer 转成有用数据
const buffer = res.value;
const dataView = new DataView(buffer);
// 假设数据格式:温度(2字节) + 湿度(2字节),都是整数(实际要除以100)
const temp = dataView.getInt16(0, true) / 100;
const hum = dataView.getInt16(2, true) / 100;
this._updateData(temp, hum);
});
},
// 更新数据并检查报警
_updateData(temp, hum) {
let alertLevel = 0;
if (temp > 35 || hum > 80) alertLevel = 2; // 危险
else if (temp > 30 || hum > 70) alertLevel = 1; // 预警
const history = this.data.history;
history.unshift({
temp,
hum,
time: new Date().toLocaleTimeString()
});
if (history.length > 20) history.pop(); // 只保留20条
this.setData({
temperature: temp.toFixed(1),
humidity: hum.toFixed(1),
alertLevel,
history
});
// 危险报警
if (alertLevel === 2) {
uni.vibrateLong({}); // 震动提醒
uni.showModal({
title: "⚠️ 报警",
content: `温度 ${temp}℃ 或湿度 ${hum}% 超标!`,
showCancel: false
});
}
},
// 断开连接
disconnect() {
if (this.data.deviceId) {
uni.closeBLEConnection({
deviceId: this.data.deviceId
});
}
this.setData({
connected: false,
deviceId: null,
temperature: "--",
humidity: "--"
});
},
onUnload() {
this.disconnect();
uni.closeBluetoothAdapter();
}
});
界面代码(monitor.wxml):
<!-- pages/sensor/monitor.wxml -->
<view class="container">
<!-- 状态栏 -->
<view class="status-bar {{connected ? 'online' : 'offline'}}">
{{connected ? '已连接传感器' : '未连接'}}
</view>
<!-- 温湿度显示 -->
<view class="data-panel">
<view class="data-item">
<text class="label">温度</text>
<text class="value {{alertLevel === 2 ? 'danger' : ''}}">
{{temperature}}°C
</text>
</view>
<view class="data-item">
<text class="label">湿度</text>
<text class="value {{alertLevel === 2 ? 'danger' : ''}}">
{{humidity}}%
</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="btn-group">
<button bindtap="connectSensor">连接设备</button>
<button bindtap="disconnect" type="warn">断开</button>
</view>
<!-- 历史记录 -->
<view class="history">
<text class="title">最近记录</text>
<view wx:for="{{history}}" wx:key="time" class="record-item">
{{item.time}} - 温度 {{item.temp}}°C / 湿度 {{item.hum}}%
</view>
</view>
</view>
预期输出:
[状态栏] 已连接传感器
[温湿度] 温度 26.5°C 湿度 65%
[按钮] [连接设备] [断开]
[最近记录]
10:30:25 - 温度 26.5°C / 湿度 65%
10:30:20 - 温度 26.4°C / 湿度 64%
10:30:15 - 温度 26.6°C / 湿度 66%
💪 进阶:3 个坑 + 1 个技巧
坑 1:蓝牙适配器没打开就搜索
// ❌ 错误:没检查适配器状态就直接搜索
uni.startBluetoothDevicesDiscovery({
success: (res) => { console.log("搜索中..."); }
});
// ✅ 正确:先打开适配器,等成功回调再搜索
uni.openBluetoothAdapter({
success: () => {
uni.startBluetoothDevicesDiscovery({ /* ... */ });
},
fail: () => {
uni.showToast({ title: "请开启蓝牙" });
}
});
坑 2:NFC 标签数据格式不对导致崩溃
// ❌ 错误:直接 JSON.parse,不做容错
const data = JSON.parse(tag.buffer); // 如果不是JSON就崩
// ✅ 正确:try-catch 包裹,并做格式校验
let data;
try {
const decoder = new TextDecoder('utf-8');
data = JSON.parse(decoder.decode(tag.buffer));
} catch (e) {
data = { type: "unknown" };
}
if (data.type !== "expense") return;
坑 3:蓝牙连接后不监听数据变化就收消息
// ❌ 错误:连上就直接读,读不到怪谁?
this.connectDevice(deviceId);
uni.readBLECharacteristicValue({ /* ... */ }); // 可能还没准备好
// ✅ 正确:订阅 notify,等回调里有数据再读
uni.notifyBLECharacteristicValueChange({
deviceId, serviceId, characteristicId,
state: true,
success: () => {
// 只有订阅了,才会收到 onBLECharacteristicValueChange 回调
}
});
坑 4:页面卸载不关闭蓝牙连接
// ❌ 错误:页面关了,蓝牙还连着,耗电
Page({
onLoad() { this.connect(); }
});
// ✅ 正确:onUnload 里清理
Page({
onUnload() {
uni.closeBLEConnection({ deviceId: this.data.deviceId });
uni.closeBluetoothAdapter();
}
});
坑 5:蓝牙特征值的 UUID 写死
// ❌ 错误:复制别人的代码,UUID 换了自己设备就不行
const SERVICE_UUID = "0000FFE0-0000-1000-8000-00805F9B34FB";
// ✅ 正确:动态获取服务和特征
uni.getBLEDeviceServices({
success: (res) => {
// 遍历所有服务,找你要的那个
res.services.forEach(s => {
if (s.uuid.includes("FFE")) { /* 匹配逻辑 */ }
});
}
});
调试技巧:console.log 加上时间戳
// 简单粗暴但有效:每个 log 前面加时间
console.log(`[${new Date().toLocaleTimeString()}] 收到数据:`, buffer);
// 如果数据是 ArrayBuffer,想看内容:
const arr = new Uint8Array(buffer);
console.log("原始字节:", Array.from(arr).map(b => b.toString(16)));
✏️ 练习题
练习 1(2 分钟):改设备名
- 输入:项目 1 的蓝牙扫描器代码
- 预期输出:把「未知设备」改成「设备名字未知」
- 提示:只改一个字符串
练习 2(3 分钟):加筛选条件
- 输入:项目 1 的代码
- 预期输出:只显示信号强度(RSSI)大于 -70 的设备
- 提示:在 _addDevice 里加一个 if 判断
练习 3(5 分钟):NFC 标签分类
- 输入:项目 2 的 NFC 记账本
- 预期输出:把 type: "expense" 改成支持「收入」和「支出」两种
- 提示:加一个 type: "income" 的判断,金额变成正数
练习 4(8 分钟):温湿度历史记录导出
- 输入:项目 3 的蓝牙监控器
- 预期输出:加一个「导出 CSV」按钮,把历史记录保存成文件
- 提示:用到 uni.saveFile,数据转成逗号分隔的字符串
练习 5(5 分钟):看报错分析原因
- 输入:以下报错信息
onBLECharacteristicValueChange:fail not available
- 预期输出:写出原因和修复方案
- 提示:特征值状态没有正确订阅
作业:做一个「蓝牙+NFC 综合工具」
需求描述:
做一个 uniapp 小工具,结合蓝牙和 NFC,实现:门禁卡管理 + 访客登记
功能点:
1. NFC 读卡:碰一下门禁卡,读出卡号,判断是「员工卡」还是「访客卡」
2. 蓝牙开门:员工卡刷成功后,自动连接蓝牙门锁发送开锁指令
3. 访客登记:如果是访客卡,刷完后弹窗让保安登记访客信息(姓名、电话、来访事由)
加分项:
1. 把登记记录存到本地 Storage,支持按日期查询
2. 支持生成访客临时卡(模拟 NFC 写入)
验收标准:
- 能正确区分员工卡和访客卡
- 能模拟开锁指令发送
- 能登记和查询访客记录
📚 总结
本文学了 3 件事:
1. 蓝牙三步走:开适配器 → 搜索 → 连接通信
2. NFC 不需要电,碰一下就读写数据
3. 蓝牙适合持续连接传数据,NFC 适合快速一次性交互
延伸资源:
- uni-app 蓝牙开发文档
- uni-app NFC 开发文档
- 《IoT 物联网实战》:蓝牙和 NFC 的硬件协议层讲解(进阶必读)
互动钩子:你在生活里遇到过哪些「蓝牙连不上」「NFC 读不出」的抓狂时刻?评论区聊聊,老粉优先回复!
下章预告:
学会了蓝牙和 NFC,你的 App 已经能和外界的「物理世界」互动了。但光有数据还不够——下一章我们要学会把这些数据画成图表,让用户一眼就看懂。猜猜我们要用什么工具?没错,就是 ECharts!🍰

评论(0)