前言
前两篇帖子中,完成了开箱、环境搭建、DHT22 温湿度传感器驱动开发,串口终端已经能稳定输出温湿度数据。本篇是最后一篇——在已有传感器功能基础上,启用蓝牙,实现温湿度数据的无线传输,并直接用网页查看数值和变化曲线。
一、本次目标
硬件:FRDM-MCXW71 开发板 + DHT22 温湿度传感器
软件:Zephyr RTOS(蓝牙 NUS 服务)
前端:网页通过 Web Bluetooth 连接设备,实时显示温度、湿度,并绘制动态曲线
低功耗:传感器间歇供电,系统空闲可进入轻度睡眠(后续再优化深度睡眠)
二、硬件连接与设备树配置
DHT22 数据引脚接开发板的 PTD1,VCC 通过一个 GPIO(PTA0)控制通断,以便采样后断电省电。
设备树 overlay(app.overlay):
/*
* Copyright (c) 2023 Ian Morris
*
* SPDX-License-Identifier: Apache-2.0
*/
/ {
dht22: dht22 {
compatible = "aosong,dht";
dio-gpios = <&gpiod 1 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
dht22;
status = "okay";
};
aliases {
dht-dev = &dht22;
};
};
// 确保 GPIOD 时钟已开启
&gpiod {
status = "okay";
};prj.conf 基础配置:
CONFIG_SERIAL=y CONFIG_CONSOLE=y CONFIG_PRINTK=y CONFIG_ADC=y CONFIG_BT=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_GATT_CLIENT=y CONFIG_BT_GATT_AUTO_UPDATE_MTU=y CONFIG_BT_ZEPHYR_NUS=y CONFIG_BT_DEVICE_NAME="DHT22_Sensor" CONFIG_BT_BUF_ACL_RX_SIZE=251 CONFIG_BT_BUF_ACL_TX_SIZE=251 CONFIG_BT_L2CAP_TX_MTU=247 CONFIG_BLE_1S_TX_INTERVAL_MS=1000 CONFIG_STDOUT_CONSOLE=y CONFIG_SENSOR=y CONFIG_SENSOR_ASYNC_API=y CONFIG_LOG=y CONFIG_DHT=y CONFIG_GPIO=y

1. DHT22 读取函数
利用 Zephyr Sensor API,直接获取温度和湿度(浮点数):
static int read_dht22(const struct device *dev, float *temp, float *humid)
{
struct sensor_value temp_val, humid_val;
int ret;
ret = sensor_sample_fetch(dev);
if (ret < 0) return ret;
ret = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp_val);
if (ret < 0) return ret;
ret = sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humid_val);
if (ret < 0) return ret;
*temp = sensor_value_to_float(&temp_val);
*humid = sensor_value_to_float(&humid_val);
return 0;
}2. 格式化发送数据(避开浮点打印坑)
Zephyr 的 printk / snprintk 默认不支持 %f,直接打印会出现 *float*。解决方案:将浮点数乘以 10,转为整数,拆分为整数和小数部分打印。
static int format_sensor_payload(char *buf, size_t buf_size,
uint32_t sample_count,
float temp_c, float humidity)
{
int temp_int = (int)(temp_c * 10.0f + 0.5f);
int hum_int = (int)(humidity * 10.0f + 0.5f);
return snprintk(buf, buf_size,
"n=%" PRIu32 " T=%d.%dC RH=%d.%d%%\r\n",
sample_count,
temp_int / 10, temp_int % 10,
hum_int / 10, hum_int % 10);
}主函数:
/* ==================== 主函数 ==================== */
int main(void)
{
char tx_buf[TX_BUFFER_SIZE];
uint32_t tx_count = 0U;
int err;
printk("Bluetooth DHT22 sensor transmitter started\n");
/* ---- 获取 DHT22 设备(使用节点标签 dht22) ---- */
const struct device *dht_dev = DEVICE_DT_GET(DT_NODELABEL(dht22));
if (!device_is_ready(dht_dev)) {
printk("DHT22 device not ready\n");
return -ENODEV;
}
printk("DHT22 found and ready\n");
/* ---- 注册蓝牙回调 ---- */
bt_conn_cb_register(&conn_callbacks);
bt_gatt_cb_register(&gatt_callbacks);
err = bt_nus_cb_register(&nus_callbacks, NULL);
if (err) {
printk("Failed to register NUS callbacks: %d\n", err);
return err;
}
/* ---- 初始化蓝牙 ---- */
err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed: %d\n", err);
return err;
}
printk("Bluetooth initialized\n");
/* ---- 开始广播 ---- */
err = start_advertising();
if (err) {
return err;
}
/* ---- 主循环 ---- */
while (true) {
k_sleep(TX_INTERVAL);
if (!notifications_enabled) {
continue;
}
float temp, hum;
err = read_dht22(dht_dev, &temp, &hum);
if (err < 0) {
printk("DHT22 read error: %d\n", err);
continue;
}
int payload_len = format_sensor_payload(tx_buf, sizeof(tx_buf),
tx_count, temp, hum);
if (payload_len < 0) {
printk("Payload formatting error: %d\n", payload_len);
continue;
}
err = bt_nus_send(NULL, tx_buf, (uint16_t)payload_len);
if (err == 0) {
printk("Sent packet %" PRIu32 ": %s", tx_count, tx_buf);
tx_count++;
} else if (err == -EMSGSIZE) {
printk("Payload exceeds ATT MTU: %u bytes\n",
(uint32_t)payload_len);
} else if (err != -EAGAIN && err != -ENOTCONN) {
printk("Failed to send: %d\n", err);
}
}
return 0;
}notifications_enabled:由蓝牙连接回调更新(当客户端订阅 CCCD 时置 true,断开时置 false)。避免无连接时无效操作。
传感器读取:read_dht22 为自定义底层驱动函数,返回负值表示失败(如超时、CRC 错误)。
数据格式化:format_sensor_payload 将序号、温度、湿度按约定格式写入 tx_buf,返回实际字符串长度。若缓冲区不足则返回负值。
发送逻辑:
bt_nus_send(NULL, ...):NULL 表示使用默认连接(如果存在多连接,需传入指定连接对象)。
发送成功(返回 0)时打印日志并递增计数器。
三、Web Bluetooth 前端
在这里UI呈现的方式选择了浏览器,浏览器端使用 Web Bluetooth API 连接设备,订阅 NUS TX 通知,解析数据并显示。
在此之前,我没听过浏览器调用蓝牙进行数据传输,这次确实学到了东西!
我大致了解了下原理: 设备(作为蓝牙外设)通过 Nordic UART Service (NUS) 发送数据,NUS 就像一个虚拟串口,它包含 TX 和 RX 两个 Characteristic。网页端(作为蓝牙中心)连接设备后,会订阅 TX Characteristic 的 通知(Notification),一旦你的设备通过 bt_nus_send() 发送数据,网页就会立刻收到通知并更新显示。
1. 连接与订阅流程
// UUID 常量
const NUS_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e';
const NUS_TX_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e';
async function connect() {
const device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'DHT22_Sensor' }],
optionalServices: [NUS_SERVICE_UUID]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(NUS_SERVICE_UUID);
const txChar = await service.getCharacteristic(NUS_TX_UUID);
await txChar.startNotifications();
txChar.addEventListener('characteristicvaluechanged', handleNotification);
}2. 解析数据并更新 UI
使用 Chart.js 库,创建双轴折线图,温度和湿度分别用左右 Y 轴,保留最近 30 个点位数据。function handleNotification(event) {
const text = new TextDecoder('utf-8').decode(event.target.value);
const tempMatch = text.match(/T=([\d.]+)C/);
const humMatch = text.match(/RH=([\d.]+)%/);
if (tempMatch && humMatch) {
const temp = parseFloat(tempMatch[1]);
const hum = parseFloat(humMatch[1]);
// 更新温度/湿度显示
document.getElementById('tempDisplay').textContent = temp.toFixed(1);
document.getElementById('humDisplay').textContent = hum.toFixed(1);
// 添加数据到图表
addDataPoint(temp, hum);
}
}3. 加入实时曲线图
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: '温度 (°C)', data: [], borderColor: 'rgb(255,99,132)', yAxisID: 'y' },
{ label: '湿度 (%)', data: [], borderColor: 'rgb(54,162,235)', yAxisID: 'y1' }
]
},
options: {
scales: {
y: { min: 0, max: 50, position: 'left' },
y1: { min: 0, max: 100, position: 'right', grid: { drawOnChartArea: false } }
}
}
});
function addDataPoint(temp, hum) {
const now = new Date().toLocaleTimeString();
if (chart.data.labels.length >= 30) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
chart.data.datasets[1].data.shift();
}
chart.data.labels.push(now);
chart.data.datasets[0].data.push(temp);
chart.data.datasets[1].data.push(hum);
chart.update('none');
}
完整的代码文件我放入压缩包,大家自行下载~
四、成果图汇总
手机端蓝牙串口小程序,实时接收温湿度数据

web端蓝牙接收实时温湿度数据并保存30个温湿度点位形成曲线

五、踩坑与解决方案
问题原因解决
| 发送数据出现 *float* | snprintk 不支持 %f | 将浮点数转为整数拆分打印 |
| Web Bluetooth 扫描不到设备 | 设备名称不匹配或未广播 | 确保 CONFIG_BT_DEVICE_NAME 与网页 DEVICE_NAME 一致 |
六、总结与扩展
通过这个项目,我打通了从传感器采集、蓝牙透传到 Web 前端可视化的完整链路。核心收获:
Zephyr 的 Sensor API 和 NUS 服务 非常方便,设备树配置一次,驱动自动管理。
Web Bluetooth 让网页直接与设备交互,无需安装 App,调试和展示极其便捷。
非常感谢!EEPW和e络盟举办这么实用的活动,本次活动我学到了很多内容。并且温湿度计与大家生活息息相关,后续我会用此套方案来监测房间的温湿度环境情况。非常感谢。
我要赚赏金
