这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 成果帖:实现蓝牙功能与手机/电脑可视化

共1条 1/1 1 跳转至

成果帖:实现蓝牙功能与手机/电脑可视化

菜鸟
2026-06-21 23:19:17     打赏

前言

前两篇帖子中,完成了开箱、环境搭建、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

728ceb61325a6762b4888f0ecd40e272.jpg

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

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. 加入实时曲线图

使用 Chart.js 库,创建双轴折线图,温度和湿度分别用左右 Y 轴,保留最近 30 个点位数据。

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');
}


完整的代码文件我放入压缩包,大家自行下载~

四、成果图汇总

手机端蓝牙串口小程序,实时接收温湿度数据

image.png

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

image.png

五、踩坑与解决方案


问题原因解决

发送数据出现 *float*snprintk 不支持 %f将浮点数转为整数拆分打印
Web Bluetooth 扫描不到设备设备名称不匹配或未广播确保 CONFIG_BT_DEVICE_NAME 与网页 DEVICE_NAME 一致

六、总结与扩展

通过这个项目,我打通了从传感器采集、蓝牙透传到 Web 前端可视化的完整链路。核心收获:

  • Zephyr 的 Sensor API 和 NUS 服务 非常方便,设备树配置一次,驱动自动管理。

  • Web Bluetooth 让网页直接与设备交互,无需安装 App,调试和展示极其便捷。


非常感谢!EEPW和e络盟举办这么实用的活动,本次活动我学到了很多内容。并且温湿度计与大家生活息息相关,后续我会用此套方案来监测房间的温湿度环境情况。非常感谢。

eepw源码.zip


共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]