第8章 8.3 蓝牙与 NFC

🎯 开场:你的手机为什么能刷公交卡?

你有没有想过一个问题——

公交卡明明没电池、没充电,怎么往里面充钱的?地铁闸机也没联网,怎么知道你这张卡里有没有钱?

因为有一种技术叫 NFC(近场通信),它不需要电,只需要一个「感应」就能传递信息。就像两个人击掌,不用说话、不用微信,碰一下就传递了信号。

这还没完——

你用手机蓝牙开过智能门锁吗?手机明明没连网,怎么就「滴」一声开了门?这就是蓝牙的力量,它不需要 WiFi,不需要互联网,靠自己就能两个设备之间「聊悄悄话」。

痛点来了:你以为这些功能很复杂,手机 App 开发者才能做?错!uniapp 早就把这些能力封装好了,几行代码你就能调用。

这一章,我们用 Python 的思维(对,你没看错,用写 Python 的逻辑)来理解 uniapp 的蓝牙和 NFC 开发。

学完本文你能做到
- 用代码搜索附近的蓝牙设备
- 连接蓝牙设备并读写数据
- 实现一个 NFC 模拟器(模拟刷门禁卡)
- 做一个「蓝牙控制灯」的小工具

\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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!🍰

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。