【eDIY】蓝牙低功耗温湿度计 03 过程贴:BLE通信与微信小程序任务目标
实现基础任务第三项:蓝牙功能的实现,启用蓝牙相关配置选项,使用 NXP 蓝牙APP与开发板建立连接;
实现进阶任务第二项:制作微信小程序,并实现实时环境监测。
任务实现
Kconfig 配置(prj.conf)修改
首先在开发板上打开 Zephyr 蓝牙相关的配置选项,修改 prj.conf,启动蓝牙协议栈基础支持,使能外设角色,设置蓝牙广播名,使能动态GATT服务注册,设置最大连接数,如下表:

设备树修改
需要在设备树中定义蓝牙 HCI 接口,如下所示:
&hci {
status = "okay";
};
&nbu {
status = "okay";
};关键代码
蓝牙初始化
/* 初始化 BLE */
err = bt_enable(NULL);
if (err) {
LOG_ERR("Failed to enable BLE: %d", err);
return;
}
LOG_INF("BLE enabled");
/* 初始化 EHS 服务 */
err = ble_ehs_service_init();
if (err) {
LOG_ERR("Failed to init EHS service: %d", err);
return;
}
/* 开始广播 */
err = ble_ehs_start_advertising();
if (err) {
LOG_ERR("Failed to start advertising: %d", err);
return;
}广播数据设置

GATT服务定义

蓝牙功能
蓝牙相关代码分布在以下文件中:

整体架构流程图

BLE 线程详解
所在文件 ble_thread.c ,通过宏定义 K_THREAD_DEFINE() 定义了蓝牙线程,线程名字 ble_thread,栈大小 2048 字节,优先级6,入口函数 bel_thread_entry()。
初始化流程

主循环逻辑

GATT 服务详解
EHS 服务结构
EHS(Environmental Health Sensor) 服务是一个自定义GATT服务,用于传输环境传感器数据。

数据包结构

GATT服务定义代码

广播配置
广播数据结构

广播流程

连接与通知机制
连接状态管理

数据通知流程

线程间数据共享
sensor_data 模块设计

数据同步机制
同步机制说明
互斥锁保护:使用 k_mutex 保护共享数据结构,防止并发访问冲突
有效性标志:valid 字段标识数据是否有效,消费者需检查此标志
时间戳:timestamp 记录数据采集时间,便于消费者判断数据新鲜度
非阻塞读取:sensor_data_get() 使用 k_mutex_lock(K_NO_WAIT) 避免阻塞
微信小程序
实际上在调试过程中使用了 nRF Connect 手机APP,扫描、连接 FRDM-MCXW71 广播,查看传感器数据。
之后再开发微信小程序,遇到一些问题,例如数据不缺、数据丢失的问题,好在最后解决了。
用于通过蓝牙BLE连接 NXP MCXW71 开发板上的 SEN66 空气质量传感器,实现实时数据监测和历史曲线展示。
核心功能

监测的传感器数据(9种)
温度 (Temperature) - 单位: °C
湿度 (Humidity) - 单位: %RH
PM1.0 - 单位: μg/m³
PM2.5 - 单位: μg/m³
PM4.0 - 单位: μg/m³
PM10 - 单位: μg/m³
CO₂ - 单位: ppm
VOC 指数 - 挥发性有机化合物
NOx 指数 - 氮氧化物
项目结构
nxp_ble_ht_miniprogrm/ ├── app.js # 全局应用逻辑 ├── app.json # 应用配置 ├── app.wxss # 全局样式 ├── project.config.json # 项目配置 ├── .gitignore # Git 忽略规则 └── pages/ ├── realtime/ # 实时数据页面 │ ├── realtime.js # 页面逻辑 │ ├── realtime.wxml # 页面模板 │ └── realtime.wxss # 页面样式 └── history/ # 历史曲线页面 ├── history.js # 页面逻辑 ├── history.wxml # 页面模板 └── history.wxss # 页面样式
系统流程图
BLE 连接流程图
┌─────────────────────────────────────────────────────────────────────────────┐ │ BLE 连接与数据接收流程 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────┐ │ 用户点击 │ │ "扫描连接" │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ resetBLEState│ ←── 清理旧连接状态 │ (清理状态) │ └──────┬──────┘ │ 延迟 500ms ▼ ┌─────────────┐ │openBluetooth│ │ Adapter │ ←── 打开蓝牙适配器 └──────┬──────┘ │ ▼ ┌─────────────┐ │startDiscovery│ ←── 开始搜索 BLE 设备 │ (搜索设备) │ └──────┬──────┘ │ ▼ ┌─────────────┐ ┌─────────────┐ │onBluetooth │────▶│ 匹配设备名 │ │ DeviceFound │ │ BLE/HT/Meter│ └──────┬──────┘ └──────┬──────┘ │ │ │ 找到目标设备 │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │stopDiscovery│────▶│connectToDevice│ │ (停止搜索) │ │ (连接设备) │ └─────────────┘ └──────┬──────┘ │ ▼ ┌─────────────┐ │ setBLEMTU │ ←── MTU 协商 (247字节) │ (MTU协商) │ └──────┬──────┘ │ ▼ ┌─────────────┐ │discoverServices│ ←── 发现 BLE 服务 │ (发现服务) │ └──────┬──────┘ │ ▼ ┌─────────────┐ │discoverChar │ ←── 发现特征值 │ (发现特征) │ └──────┬──────┘ │ ▼ ┌─────────────┐ │enableNotify │ ←── 启用数据通知 │ (启用通知) │ └──────┬──────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 数据接收循环 │ └─────────────────────────────────────────────────────────────────────────────┘ │ ┌──────────────────┴──────────────────┐ │ │ ▼ │ ┌─────────────┐ │ │onBLECharValue│ ←── 接收 BLE 数据包 │ │ Change │ │ └──────┬──────┘ │ │ │ ▼ │ ┌─────────────┐ │ │handleSensor │ ←── 解析 22 字节传感器数据 │ │ Data │ │ └──────┬──────┘ │ │ │ ├──────────────┬──────────────┐ │ │ │ │ │ ▼ ▼ ▼ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │更新全局 │ │添加历史 │ │更新UI显示 │ │ │ currentData│ │ Records │ │ Display │ │ └───────────┘ └─────┬─────┘ └───────────┘ │ │ │ ▼ │ ┌─────────────┐ │ │saveToStorage│ │ │ (本地存储) │ │ └─────────────┘ │ │ ┌──────────────────────────────────────┘ │ (持续监听) ▼ [循环接收数据]
数据解析流程图
┌─────────────────────────────────────────────────────────────────────────────┐ │ 传感器数据包解析流程 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ BLE 数据包结构 (22 字节, 小端序) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 偏移 │ 字段 │ 类型 │ 长度 │ 说明 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 0 │ pm1_0 │ uint16 │ 2 │ PM1.0 浓度 (×10) │ │ 2 │ pm2_5 │ uint16 │ 2 │ PM2.5 浓度 (×10) │ │ 4 │ pm4_0 │ uint16 │ 2 │ PM4.0 浓度 (×10) │ │ 6 │ pm10 │ uint16 │ 2 │ PM10 浓度 (×10) │ │ 8 │ humidity │ int16 │ 2 │ 湿度 (×100) │ │ 10 │ temp │ int16 │ 2 │ 温度 (×200) │ │ 12 │ voc │ int16 │ 2 │ VOC 指数 (×10) │ │ 14 │ nox │ int16 │ 2 │ NOx 指数 (×10) │ │ 16 │ co2 │ uint16 │ 2 │ CO₂ 浓度 │ │ 18 │ timestamp│ uint32 │ 4 │ 时间戳 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 总计: 22 字节 │ └─────────────────────────────────────────────────────────────────────────────┘ 解析步骤: ┌─────────────┐ │ 接收 Buffer │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ DataView │ ←── 创建 DataView 用于读取 │ 解析 │ └──────┬──────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 数据转换公式 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ PM 值: raw / 10.0 → μg/m³ │ │ 温度: raw / 200.0 → °C (范围: -10 ~ 80) │ │ 湿度: raw / 100.0 → %RH (范围: 0 ~ 100) │ │ VOC/NOx: raw / 10.0 → 指数值 │ │ CO₂: raw → ppm (上限: 40000) │ └─────────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────┐ │ 更新 UI │ ←── setData() 更新页面 └─────────────┘
历史曲线绘制流程图
┌─────────────────────────────────────────────────────────────────────────────┐ │ 历史曲线绘制流程 │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────┐ │ 页面加载 │ │ /onShow │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ loadData │ ←── 加载全局历史数据 └──────┬──────┘ │ ▼ ┌─────────────┐ │ filterData │ ←── 筛选最近 24 小时数据 └──────┬──────┘ │ ▼ ┌─────────────┐ │calculateStats│ ←── 计算最大/最小/平均值 └──────┬──────┘ │ ▼ ┌─────────────┐ │ drawChart │ ←── Canvas 绘制曲线 └──────┬──────┘ │ ├──────────────┬──────────────┬──────────────┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ 绘制网格 │ │ 绘制Y轴 │ │ 绘制曲线 │ │ 绘制X轴 │ │ (10等分) │ │ 标签 │ │ 数据点 │ │ 时间标签 │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ ▼ ┌─────────────┐ │ ctx.draw() │ ←── 渲染到 Canvas └─────────────┘ 自动更新机制: ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ setInterval │────▶│ 检查记录数 │────▶│ 有新数据? │ │ (2秒) │ │ 变化 │ │ │ └─────────────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ ▼ │ ┌─────────────┐ │ │ refreshChart│ │ │ (刷新曲线) │ │ └─────────────┘ │ ▼ (无变化) [继续监听]
关键代码分析
BLE 设备搜索与连接
关键点:
连接前必须清理旧状态,否则第二次连接会失败
Android 设备需要位置权限才能扫描 BLE
小米/红米手机有特殊权限处理逻辑
// 关键代码: BLE 连接流程
connectBLE() {
// 1. 先清理旧状态(避免第二次连接失败)
this.resetBLEState();
// 2. 延迟 500ms 让 BLE 栈完全清理
setTimeout(() => {
// 3. 打开蓝牙适配器
wx.openBluetoothAdapter({
success: () => {
// 4. 开始搜索设备
this.startDiscovery();
}
});
}, 500);
}
// 设备匹配逻辑
wx.onBluetoothDeviceFound((res) => {
res.devices.forEach(device => {
const name = device.name || device.localName || '';
// 匹配设备名称关键词
if (name.includes('BLE') || name.includes('HT') ||
name.includes('Meter') || name.includes('SEN66')) {
// 停止搜索并连接
wx.stopBluetoothDevicesDiscovery();
this.connectToDevice(device.deviceId);
}
});
});MTU 协商与数据接收
关键点:
MTU 协商到 247 字节,确保能接收完整的 22 字节传感器数据包
启用 Notify 特征值才能接收设备主动推送的数据
// MTU 协商 - 关键! 确保能接收完整数据包
wx.setBLEMTU({
deviceId,
mtu: 247, // 协商到 247 字节
success: (res) => {
console.log('MTU 协商成功:', res.mtu);
// MTU 协商完成后发现服务
setTimeout(() => {
this.discoverServices();
}, 500);
}
});
// 启用数据通知
wx.notifyBLECharacteristicValueChange({
deviceId: this.data.deviceId,
serviceId: this.data.serviceId,
characteristicId: characteristicId,
state: true
});
// 数据监听
wx.onBLECharacteristicValueChange((res) => {
this.handleSensorData(res.value);
});传感器数据解析
关键点:
使用 DataView 的 getUint16/getInt16 方法,第二个参数 true 表示小端序
温度转换公式: raw / 200.0
湿度转换公式: raw / 100.0
PM 值转换公式: raw / 10.0
// 解析 22 字节传感器数据包
handleSensorData(buffer) {
const dataView = new DataView(buffer);
// 数据包结构 (小端序)
const pm1_0 = dataView.getUint16(0, true); // 偏移 0
const pm2_5 = dataView.getUint16(2, true); // 偏移 2
const pm4_0 = dataView.getUint16(4, true); // 偏移 4
const pm10 = dataView.getUint16(6, true); // 偏移 6
const rawHumi = dataView.getInt16(8, true); // 偏移 8
const rawTemp = dataView.getInt16(10, true); // 偏移 10
const voc_index = dataView.getInt16(12, true); // 偏移 12
const nox_index = dataView.getInt16(14, true); // 偏移 14
const co2 = dataView.getUint16(16, true); // 偏移 16
const timestamp = dataView.getUint32(18, true); // 偏移 18
// 数据转换
const data = {
pm1_0: (pm1_0 / 10.0).toFixed(1),
pm2_5: (pm2_5 / 10.0).toFixed(1),
humidity: Math.max(0, Math.min(100, rawHumi / 100.0)).toFixed(1),
temperature: Math.max(-10, Math.min(80, rawTemp / 200.0)).toFixed(1),
voc_index: (voc_index / 10.0).toFixed(1),
nox_index: (nox_index / 10.0).toFixed(1),
co2: Math.min(40000, co2)
};
// 更新全局数据
app.globalData.currentData = data;
// 添加到历史记录
app.addHistoryRecord({...data, timestamp: Date.now()});
}历史数据存储
关键点:
使用 wx.setStorageSync 本地存储,最多保存 10000 条记录
历史数据在 App 启动时自动加载
// 全局数据管理
globalData: {
bleConnected: false,
deviceId: null,
serviceId: null,
characteristicId: null,
currentData: null,
historyRecords: []
}
// 添加历史记录(限制最多 10000 条)
addHistoryRecord(record) {
this.globalData.historyRecords.push(record);
if (this.globalData.historyRecords.length > 10000) {
this.globalData.historyRecords =
this.globalData.historyRecords.slice(-10000);
}
this.saveHistoryToStorage();
}
// 本地存储
saveHistoryToStorage() {
wx.setStorageSync('sen66_history',
this.globalData.historyRecords.slice(-10000));
}Canvas 曲线绘制
关键点:
使用微信小程序旧版 Canvas API (wx.createCanvasContext)
Y 轴范围固定,避免数据波动导致曲线跳跃
数据量大时自动采样(最多显示 200 个点)
// 绘制历史曲线
drawChart() {
const ctx = wx.createCanvasContext('mainChart', this);
// Y 轴固定范围配置
const Y_AXIS_RANGES = {
'温度': { yMin: 0, yMax: 41, unit: '°C' },
'湿度': { yMin: 0, yMax: 80, unit: '%RH' },
'PM2.5': { yMin: 0, yMax: 1200, unit: 'μg/m³' },
'CO₂': { yMin: 100, yMax: 2000, unit: 'ppm' }
};
// 绘制网格 (10 等分)
for (let i = 0; i <= 10; i++) {
const y = paddingTop + (chartHeight / 10) * i;
ctx.beginPath();
ctx.moveTo(paddingLeft, y);
ctx.lineTo(width - paddingRight, y);
ctx.stroke();
}
// 绘制曲线
ctx.beginPath();
ctx.setStrokeStyle(color);
ctx.setLineWidth(2);
data.forEach((d, i) => {
const x = paddingLeft + (chartWidth / (data.length - 1)) * i;
const y = paddingTop + chartHeight -
((d[key] - yMin) / yRange) * chartHeight;
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
ctx.draw();
}技术要点总结
BLE连接注意事项

数据协议
数据包长度: 22 字节
字节序: 小端序 (Little Endian)
传输方式: BLE Notify (设备主动推送)
更新频率: 约 5 秒/次
UI设计特点
TabBar: 两个页面(实时数据、历史曲线)
实时页面: 9 种传感器数据卡片式展示
历史页面: Canvas 曲线图 + 9 宫格快速查看
颜色编码: 每种数据类型有专属颜色
功能演示
附上手机APP演示截图。







我要赚赏金
