这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【e起DIY】低功耗蓝牙温湿度计-6,成果贴-微信小程序显示温湿度

共1条 1/1 1 跳转至

【e起DIY】低功耗蓝牙温湿度计-6,成果贴-微信小程序显示温湿度

助工
2026-06-14 18:07:42     打赏

书接上回,目前已经实现蓝牙,获取温湿度的功能,现在结合起来,然后用微信小程序通过蓝牙连接FRDM-MCXW71,然后获取温湿度数据。

现在仅有 read_vnd 与 write_vnd 也是可以把温湿度数据发给手机,开始的想法是手动查询,先手机发起获取指令,模块在write_vnd 收到指令,然后解析、获取数据,再通过 read_vnd 发给手机,但是 read_vnd 是被动获取的,也就是蓝牙调试助手中点击 read 功能才能触发 read_vnd 回调,那么这就不需要手机发送指令查询了。

但是,每次都需要点击获取,从体验上不太好,了解到有个通知的功能,只要手机订阅了通知就可以一直发数据给手机,手机会自动接收。

ble.h代码,蓝牙通信协议如下

#ifndef _BLE_H_
#define _BLE_H_

#include <stdint.h>

/**
  * @brief 蓝牙通信协议帧格式
  * 
  * 帧格式: [帧头][命令][数据长度][数据][校验][帧尾]
  * 帧头: 1字节
  * 命令: 1字节 (区分不同功能)
  * 数据长度: 1字节 (高位在前,表示数据域长度)
  * 数据: N字节 (实际数据内容)
  * 校验: 1字节 (异或校验,从帧头到数据域结束)
  * 帧尾: 1字节
  */
 
#define BLE_PROTOCOL_HEADER     0xA5    // 帧头
#define BLE_PROTOCOL_TAIL       0x5A    // 帧尾

typedef enum {
    CMD_NONE            = 0x00,  // 无操作
    CMD_LED_TOGGLE      = 0x01,  // LED翻转控制(无参数)
    CMD_LED_ON          = 0x02,  // LED开启(带参数:0x01)
    CMD_LED_OFF         = 0x03,  // LED关闭(带参数:0x00)
    CMD_GET_TEMP_HUMI   = 0x10,  // 获取温湿度数据
    CMD_GET_VOLTAGE     = 0x11,  // 获取电压
    CMD_GET_CURRENT     = 0x12,  // 获取电流
    CMD_GET_POWER       = 0x13,  // 获取功率
    CMD_GET_ALL_DATA    = 0x14,  // 获取所有数据
    CMD_DEVICE_STATUS   = 0x20,  // 查询设备状态
    CMD_DEVICE_RESET    = 0x21,  // 设备复位
    CMD_ECHO_TEST       = 0x30,  // 回环测试
} Protocol_Cmd_t;

void ble_init(void);

#endif

ble.c 代码

#include <stddef.h>  
#include <string.h>  
#include <errno.h>  
#include <zephyr/types.h>  
#include <zephyr/sys/printk.h>  
#include <zephyr/kernel.h>  
#include <zephyr/settings/settings.h>  
#include <zephyr/bluetooth/bluetooth.h>  
#include <zephyr/bluetooth/hci.h>  
#include <zephyr/bluetooth/conn.h>  
#include <zephyr/bluetooth/uuid.h>  
#include <zephyr/bluetooth/gatt.h>  

#include "ble.h"

// 自定义服务UUID (128-bit)  
#define BT_UUID_CUSTOM_SERVICE_VAL BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0)  
 
// 自定义特征UUID (128-bit)  
#define BT_UUID_CUSTOM_CHAR_VAL BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1)  
 
// 定义UUID结构体  
static const struct bt_uuid_128 vnd_svc_uuid = BT_UUID_INIT_128(BT_UUID_CUSTOM_SERVICE_VAL);  
static const struct bt_uuid_128 vnd_char_uuid = BT_UUID_INIT_128(BT_UUID_CUSTOM_CHAR_VAL);  
 
// 特征值最大长度  
#define VND_MAX_LEN 20  
 
// 特征值存储  
static uint8_t vnd_value[VND_MAX_LEN + 1] = {0};  
 
// 当前连接
static struct bt_conn *current_conn = NULL;
// 定义一个延迟工作项
static struct k_work_delayable restart_adv_work;
// 数据发送定时器
static struct k_timer data_send_timer;  
// 通知订阅状态
static bool notification_enabled = false;
// 温湿度传感器数据
double sht30_temp = 0, sht30_humi = 0;
// 添加一个普通工作项(非延迟)
static struct k_work send_data_work;

extern uint8_t sht30_read(double *temp_out, double *hum_out);

// 读取特征值回调, 被动发送数据
static ssize_t read_vnd(struct bt_conn *conn, const struct bt_gatt_attr *attr,  
            void *buf, uint16_t len, uint16_t offset)  
{  
    const char *value = attr->user_data;  // 这个是缓冲区的数据
     
    printk("Read request receivedn");  
    return bt_gatt_attr_read(conn, attr, buf, len, offset, value, strlen(value));  
}  
 
// 写入特征值回调  
static ssize_t write_vnd(struct bt_conn *conn, const struct bt_gatt_attr *attr,  
             const void *buf, uint16_t len, uint16_t offset,  
             uint8_t flags)  
{  
    uint8_t *value = attr->user_data;  
 
    if (offset + len > VND_MAX_LEN)  
    {  
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);  
    }  
    memcpy(value + offset, buf, len);  
    value[offset + len] = 0;  
    printk("Received from phone: %sn", value);  
    return len;  
}  

// CCCD 回调(手机订阅/取消订阅通知时触发)
static void vnd_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)  
{
    notification_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Notification %sn", notification_enabled ? "enabled" : "disabled");
   
    if (notification_enabled && current_conn)
    {
        // 手机订阅通知后,启动数据发送定时器
        k_timer_start(&data_send_timer, K_SECONDS(1), K_SECONDS(1));
        printk("Data send timer startedn");
    }
    else if (!notification_enabled)
    {
        // 手机取消订阅,停止定时器
        k_timer_stop(&data_send_timer);
        printk("Data send timer stoppedn");
    }
}

// 定义GATT服务(支持通知)
BT_GATT_SERVICE_DEFINE(vnd_svc,  
    BT_GATT_PRIMARY_SERVICE(&vnd_svc_uuid.uuid),  
    BT_GATT_CHARACTERISTIC(&vnd_char_uuid.uuid,  
                   BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,  
                   BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,  
                   read_vnd, write_vnd, vnd_value),  
    BT_GATT_CCC(vnd_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),  
);
 
// 广播数据  
static const struct bt_data ad[] = {  
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),  
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_CUSTOM_SERVICE_VAL),  
};  
 
// 扫描响应数据  
static const struct bt_data sd[] = {  
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),  
};  

uint8_t ble_temp_humi_Packaging(uint8_t cmd, uint8_t *src_data, uint8_t src_len, uint8_t *dst_data)
{
    if (src_len > VND_MAX_LEN - 5)
        return 0;
    uint8_t calc_checksum = 0;
    dst_data[0] = BLE_PROTOCOL_HEADER;
    dst_data[1] = cmd;
    dst_data[2] = src_len;
    memcpy(dst_data + 3, src_data, src_len);
   
    calc_checksum = cmd ^ src_len;
    for (int i = 0; i < src_len; i++)
    {
        calc_checksum ^= src_data[i];
    }
    dst_data[3 + src_len] = calc_checksum;
    dst_data[4 + src_len] =  BLE_PROTOCOL_TAIL;
    return (5 + src_len);
}

uint8_t ble_get_temp_humi(uint8_t *data)
{
    uint8_t err = sht30_read(&sht30_temp, &sht30_humi);
    if (err)
        return 0;
    data[0] = (uint8_t)sht30_temp;                                    // 温度整数部分
    data[1] = (uint8_t)((int)(sht30_temp * 100) % 100);              // 温度小数部分
    data[2] = (uint8_t)sht30_humi;                                    // 湿度整数部分
    data[3] = (uint8_t)((int)(sht30_humi * 100) % 100);              // 湿度小数部分
    return 4;
}
// 发送数据到手机(主动通知)
static void send_data_to_phone(void)
{
    if (!current_conn || !notification_enabled) {
        return;  // 未连接或手机未订阅通知
    }
   
    // 组包传感器数据
    uint8_t data[15] = {0}, ble_send_data_len = 0;
    uint8_t ble_send_data[VND_MAX_LEN] = {0};
    // 获取传感器数据
    ble_get_temp_humi(data);
    // 组包
    ble_send_data_len = ble_temp_humi_Packaging(CMD_GET_TEMP_HUMI, data, 4, ble_send_data);

    // 发送通知
    int err = bt_gatt_notify(current_conn, &vnd_svc.attrs[1],  // 特征值属性
                            ble_send_data, strlen(ble_send_data));
   
    if (err) {
        printk("Failed to send notification (err %d)n", err);
    } else {
        printk("Sent to phone successfullyn");
    }
}

// 数据发送定时器回调
static void data_send_timeout(struct k_timer *timer)
{
    // send_data_to_phone();// 不行,读取温湿度数据会失败
    k_work_submit(&send_data_work);// 提交到一个普通工作项
}
// 在工作项中执行实际的数据读取和发送
static void send_data_work_fn(struct k_work *work)
{
    send_data_to_phone();
}
// 连接回调  
static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err)
    {
        printk("Connection failed, err 0x%02x %sn", err, bt_hci_err_to_str(err));
       
        // 连接失败时也尝试重新广播
        int adv_err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
        if (adv_err)
        {
            printk("Restart advertising after connection failure failed (err %d)n", adv_err);
        }
    }
    else 
    {
        printk("Connectedn");
       
        // 停止广播
        bt_le_adv_stop();
       
        // 保存连接对象
        if (current_conn)
        {
            bt_conn_unref(current_conn);
        }
        current_conn = bt_conn_ref(conn);
    }
}
 
// 延迟重启广播的工作函数
static void restart_adv_work_fn(struct k_work *work)
{
    // 先确保广播完全停止
    bt_le_adv_stop();
    // 给协议栈一点时间完成清理
    k_sleep(K_MSEC(100));// 很重要
   
    // 使用原有的 BT_LE_ADV_CONN_FAST_1 参数
    int err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    if (err)
    {
        printk("Restart advertising failed (err %d)n", err);
    }
    else 
    {
        printk("Restart advertising successfullyn");
    }
}
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    printk("Disconnected, reason 0x%02x %sn", reason, bt_hci_err_to_str(reason));
    // 停止数据发送定时器
    k_timer_stop(&data_send_timer);
    // 重置状态
    notification_enabled = false;
    if (current_conn)
    {
        bt_conn_unref(current_conn);
        current_conn = NULL;
    }
    // 调度执行:500ms 后自动调用 restart_adv_work_fn
    k_work_schedule(&restart_adv_work, K_MSEC(500));
}

// 注册连接回调  
BT_CONN_CB_DEFINE(conn_callbacks) = {  
    .connected = connected,  
    .disconnected = disconnected,  
};  
 
// 蓝牙初始化完成回调  
static void bt_ready(void)  
{
    printk("Bluetooth initializedn");  
    // if (IS_ENABLED(CONFIG_SETTINGS)) // 如果配置了 CONFIG_SETTINGS,加载设置
    // {  
    //     settings_load();  
    // }  
    // 启动广播  
    int err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));  
    if (err)  
    {  
        printk("Advertising failed to start (err %d)n", err);  
        return;  
    }  
    printk("Advertising successfully startedn");  
}  

void ble_init(void)  
{  
    int err = 0;  
    // 初始化工作项
    k_work_init(&send_data_work, send_data_work_fn);
    // 初始化数据发送定时器
    k_timer_init(&data_send_timer, data_send_timeout, NULL);
    // 初始化一个可延迟执行的工作项, 把 restart_adv_work 和它的执行函数 restart_adv_work_fn 绑定
    k_work_init_delayable(&restart_adv_work, restart_adv_work_fn);
    // 初始化蓝牙  
    err = bt_enable(NULL);  
    if (err != 0)  
    {  
        printk("Bluetooth init failed (err %d)n", err);  
        return;  
    }  
    // 注册蓝牙初始化完成回调
    bt_ready();  
}

在main()中实现

int main(void)  
{  
    // double temp = 0, hum = 0;
    printf("Hello World! %sn", CONFIG_BOARD_TARGET);
    // led_init();
    ble_init();
    sht30_init();

    while (1)  
    {  
        // sht30_read(&temp, &hum);
        // printf("Temperature: %.2f Cel, Humidity: %.2f %%RHn", temp, hum);
        // led_toggle();
        k_sleep(K_SECONDS(1));
    }  
    return 0;  
}

开始的时候,我是用定时器即在data_send_timeout中来把温湿度数据获取之后发给手机,但是会发现获取失败,我同步在main()中获取是正常的,说明定时器获取会产生问题,问deepseek : “在 Zephyr 中,timer 回调是在系统工作队列(system workqueue)中执行的,但该工作队列的优先级较高,且 I2C 操作可能因为某些原因失败。”具体原因就不清楚了,之后再找找原因。先采取将温湿度读取移到低优先级线程,使用低优先级的工作队列来发送通知,所以使用工作项来进行发送。

步骤为:手机订阅通知->先定时器触发->提交到工作项->工作项执行温湿度获取数据,发送数据到手机蓝牙

输出log

---- 已打开串行端口 COM25 ----
Hello World! frdm_mcxw71/mcxw716c
SHT30 initialized successfully
*** Booting Zephyr OS build v4.4.0 ***
[00:00:00.171,447] <inf> bt_hci_core: HCI transport: BT NXP
[00:00:00.171,508] <inf> bt_hci_core: Identity: 00:60:37:EA:3F:39 (public)
[00:00:00.171,539] <inf> bt_hci_core: HCI: version 6.0 (0x0e) revision 0x8300, manufacturer 0x0025
[00:00:00.171,569] <inf> bt_hci_core: LMP: version 6.0 (0x0e) subver 0x1400
Bluetooth initialized
Advertising successfully started
Connected
Disconnected, reason 0x3d 
Restart advertising successfully
Connected
Notification enabled
Data send timer started
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Sent to phone successfully
Notification disabled
Data send timer stopped

订阅通知.gif


接下来就要实现微信小程序了,这需要注册微信小程序,具体可以看一下这篇博文 微信小程序注册流程及APPID获取(完整版图文教程)_微信小程序appid-CSDN博客

之后就需要下载开发工具:下载

下载完成之后就进入开发,打开微信开发者工具,扫码登录,点击 "+" 或 "创建项目",填写信息:

  • 项目名称:温湿度监测

  • 目录:选择一个空文件夹

  • AppID:填写注册时获得的AppID

  • 开发模式:选择"小程序"

  • 模板选择:"不使用模板"

  • 点击"确定"

3,创建微信小程序项目.png

之后生成的文件结构,最主要就是这几个文件

pages/
  index/
    index.js
    index.wxml
    index.wxss
app.js
app.json
app.wxss

4,文件结构.png代码.rar,补充代码文件

具体的代码就不做分析,板子与小程序的代码皆放在附件中,下面是成果视频




关键词: 蓝牙     湿度计     成果     显示     微信小程序    

共1条 1/1 1 跳转至

回复

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