这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【e起DIY】基于蓝牙温湿度综合提交

共1条 1/1 1 跳转至

【e起DIY】基于蓝牙温湿度综合提交

菜鸟
2026-06-13 10:33:46     打赏

【前言】

在前面我实现了dht11的数据获取。这一篇将分享如何添加蓝牙功能,并在电阻端使用python实现优质的页面展示。

【功能总览】

image.png

【实现步骤】

经过对老师的课件的以及在eepw论坛中的帖子的学习。我由以下几个步骤来实现。

1、在prj.conf中添加蓝牙模版支持,修改后文件源码如下:

# DHT11 + BLE peripheral
CONFIG_SENSOR=y
CONFIG_DHT=y
CONFIG_GPIO=y
CONFIG_LOG=y
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096

# Bluetooth LE
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="LWX_demo"
CONFIG_BT_DEVICE_NAME_MAX=20
CONFIG_BT_PRIVACY=n
CONFIG_BT_DEVICE_APPEARANCE=0
CONFIG_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y

2、在main.c中添加蓝牙功能支持

2.1  做为蓝牙的peripheral设备,需要添加名字,我这里定义"LWX_demo"

2.2 定义GATT service UUID:

 *   Service  UUID 12345678-1234-5678-1234-56789abcdef0
 *   Char    UUID ...-ef1  Temperature (uint8 degC, read + notify)
 *   Char    UUID ...-ef2  Humidity    (uint8 %RH,  read + notify)

这三个服务一个为蓝牙发现,以及湿度、温度的读取与订阅,使得客户端可以实时获取数据,要不要周期的去读取。

2.3  根据教程,结合我自己的学习,编写main.c代码如下:

/*
 * Copyright 2026
 * SPDX-License-Identifier: Apache-2.0
 *
 * FRDM-MCXW71: DHT11 sensor + BLE peripheral ("LWX_demo").
 *
 * Hardware:
 *   DHT11 DATA -> ARDUINO D4 (PTA19, J2 pin 8)
 *   DHT11 VCC  -> 3.3V, GND -> GND
 *   [external 4.7k..10k pull-up on DATA]
 *
 * GATT service (custom, no SIG-assigned):
 *   Service  UUID 12345678-1234-5678-1234-56789abcdef0
 *   Char    UUID ...-ef1  Temperature (uint8 degC, read + notify)
 *   Char    UUID ...-ef2  Humidity    (uint8 %RH,  read + notify)
 *
 * NBU BLE firmware must be flashed separately (see frdm_mcxw71 doc).
 */

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/sys/printk.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/settings/settings.h>

#include "dht11.h"

#define DHT11_PIN 19
#define READ_PERIOD_MS 2000

/* File-level adv params so disconnected() can re-start advertising. */
static const struct bt_le_adv_param adv_params = {
        .id = BT_ID_DEFAULT,
        .sid = 0,
        .secondary_max_skip = 0,
        .options = BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_USE_IDENTITY,
        .interval_min = BT_GAP_ADV_FAST_INT_MIN_1,
        .interval_max = BT_GAP_ADV_FAST_INT_MAX_1,
        .peer = NULL,
    };


/* ---- Custom GATT service for DHT11 ---- */
static const struct bt_uuid_128 svc_uuid = BT_UUID_INIT_128(
    BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef0));
static const struct bt_uuid_128 temp_chr_uuid = BT_UUID_INIT_128(
    BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef1));
static const struct bt_uuid_128 hum_chr_uuid = BT_UUID_INIT_128(
    BT_UUID_128_ENCODE(0x12345678, 0x1234, 0x5678, 0x1234, 0x56789abcdef2));

static uint8_t temp_value = 0;   /* degC, integer */
static uint8_t hum_value  = 0;   /* %RH, integer */

static bool temp_notify_enabled;
static bool hum_notify_enabled;

static ssize_t read_u8(struct bt_conn *conn, const struct bt_gatt_attr *attr,
               void *buf, uint16_t len, uint16_t offset)
{
    const uint8_t *value = attr->user_data;
    return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(*value));
}

static void temp_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    temp_notify_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Temperature notify %s\n", temp_notify_enabled ? "enabled" : "disabled");
}

static void hum_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
    hum_notify_enabled = (value == BT_GATT_CCC_NOTIFY);
    printk("Humidity notify %s\n", hum_notify_enabled ? "enabled" : "disabled");
}

BT_GATT_SERVICE_DEFINE(dht_svc,
    BT_GATT_PRIMARY_SERVICE(&svc_uuid),
    BT_GATT_CHARACTERISTIC(&temp_chr_uuid.uuid,
                   BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
                   BT_GATT_PERM_READ,
                   read_u8, NULL, &temp_value),
    BT_GATT_CCC(temp_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    BT_GATT_CHARACTERISTIC(&hum_chr_uuid.uuid,
                   BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
                   BT_GATT_PERM_READ,
                   read_u8, NULL, &hum_value),
    BT_GATT_CCC(hum_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)),
    /* Expose 128-bit service UUID so scanners can filter on it. */
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0xef, 0xcd, 0xab, 0x89, 0x67, 0x45, 0x34, 0x12,
              0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12),
};

static const struct bt_data sd[] = {
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};

static void adv_restart_work_handler(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(adv_restart_work, adv_restart_work_handler);

static void adv_restart_work_handler(struct k_work *work)
{
    int err = bt_le_adv_start(&adv_params, ad, ARRAY_SIZE(ad),
                       sd, ARRAY_SIZE(sd));
    if (err) {
        printk("Advertising restart failed (err %d)\n", err);
    } else {
        printk("Advertising restarted\n");
    }
}

static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        printk("Connection failed (err 0x%02x)", err);
    } else {
        printk("Connected\n");
    }
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{   printk("Disconnected (reason 0x%02x)\n", reason);
    temp_notify_enabled = false;
    hum_notify_enabled  = false;
    /* Re-start advertising on a work item, with a short delay so the
     * BLE stack can release internal resources first. */
    k_work_schedule(&adv_restart_work, K_MSEC(50));

}

BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
};

static void bt_ready(int err)
{
    if (err) {
        printk("Bluetooth init failed (err %d)\n", err);
        return;
    }
    printk("Bluetooth initialized\n");

    if (IS_ENABLED(CONFIG_SETTINGS)) {
        settings_load();
    }

    err = bt_le_adv_start(&adv_params, ad, ARRAY_SIZE(ad),
                  sd, ARRAY_SIZE(sd));
    if (err) {
        printk("Advertising failed to start (err %d)\n", err);
        return;
    }
    printk("Advertising as \"%s\"\n", CONFIG_BT_DEVICE_NAME);
}

int main(void)
{
    int err;
    const struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpioa));

    if (gpio_dev == NULL || !device_is_ready(gpio_dev)) {
        printk("ERROR: GPIOA unavailable\n");
        return 0;
    }

    err = dht11_init(gpio_dev, DHT11_PIN);
    if (err) {
        printk("dht11_init failed: %d\n", err);
        return 0;
    }
    printk("DHT11 ready on PTA19 (ARDUINO D4). Polling every %d ms.\n", READ_PERIOD_MS);

    /* DHT11 first-read discard: see dht11.c comment. */
    {
        uint8_t h_d, t_d;
        (void)dht11_read(gpio_dev, DHT11_PIN, &h_d, &t_d);
    }

    err = bt_enable(bt_ready);
    if (err) {
        printk("bt_enable failed: %d\n", err);
        return 0;
    }

    while (1) {
        uint8_t h, t;
        int rc = dht11_read(gpio_dev, DHT11_PIN, &h, &t);

        if (rc == 0) {
            hum_value  = h;
            temp_value = t;
            printk("H=%d%% T=%dC\n", h, t);
            /* Push notify on every poll, not only on change, so the
             * host sees a steady stream of readings. */
            if (hum_notify_enabled) {
                bt_gatt_notify(NULL, &dht_svc.attrs[5], &hum_value, sizeof(hum_value));
            }
            if (temp_notify_enabled) {
                bt_gatt_notify(NULL, &dht_svc.attrs[2], &temp_value, sizeof(temp_value));
            }
        } else {
            printk("DHT11 read failed: %d\n", rc);
        }

        k_msleep(READ_PERIOD_MS);
    }

    return 0;
}

2.4 在main.c中,我们特别需要注意的是,客户端断开后,需要进行处理,并重启发现:

static void disconnected(struct bt_conn *conn, uint8_t reason)
{	printk("Disconnected (reason 0x%02x)\n", reason);
	temp_notify_enabled = false;
	hum_notify_enabled  = false;
	/* Re-start advertising on a work item, with a short delay so the
	 * BLE stack can release internal resources first. */
	k_work_schedule(&adv_restart_work, K_MSEC(50));

}

编译好后下载到开发板,打印日志如下:

*** Booting Zephyr OS build v4.4.0 ***
DHT11 ready on PTA19 (ARDUINO D4). Polling every 2000 ms.
[00:00:00.162,658] <inf> bt_hci_core: HCI transport: BT NXP
[00:00:00.162,719] <inf> bt_hci_core: Identity: 00:60:37:D3:EF:84 (public)
[00:00:00.162,719] <inf> bt_hci_core: HCI: version 6.0 (0x0e) revision 0x8300
[00:00:00.162,750] <inf> bt_hci_core: LMP: version 6.0 (0x0e) subver 0x1400
Bluetooth initialized
Advertising as "LWX_demo"
H=95% T=27C
H=95% T=27C

2.5 打印蓝牙调试助手,可以成功的扫描并连接到开发板上。

【远程数据连接与展示】

1、安装蓝牙python依赖:

pip install bleak

2、编写服务端代码:

#!/usr/bin/env python3
"""
BLE frontend for FRDM-MCXW71 "LWX_demo".

Connects to the device, subscribes to DHT11 temperature/humidity
characteristics, records each reading into a CSV log, and serves
a small HTTP API + dashboard for the browser.

Endpoints:
    GET  /                          -> dashboard.html
    GET  /api/current              -> JSON: {ts, temp, hum, connected}
    GET  /api/history?hours=24     -> JSON: {points: [{ts, temp, hum}, ...]}
    GET  /api/stats?hours=24       -> JSON: {count, min_t, max_t, avg_t, min_h, max_h, avg_h}
"""
import asyncio
import csv
import json
import signal
import sys
import time
from pathlib import Path
from bleak import BleakScanner, BleakClient

DEVICE_NAME = "LWX_demo"
TEMP_CHR_UUID = "12345678-1234-5678-1234-56789abcdef1"
HUM_CHR_UUID  = "12345678-1234-5678-1234-56789abcdef2"

LOG_DIR = Path(__file__).resolve().parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
LOG_FILE = LOG_DIR / "dht11.csv"

state = {"ts": None, "temp": None, "hum": None, "connected": False}
state_lock = asyncio.Lock()

# CSV header on first run.
if not LOG_FILE.exists():
    with open(LOG_FILE, "w", newline="", encoding="utf-8") as f:
        csv.writer(f).writerow(["timestamp", "temp_c", "hum_pct"])


# Cache the latest halves; we only log+publish when BOTH have arrived.
cache = {"temp": None, "hum": None, "ts": 0.0}

async def push_reading(temp, hum):
    """Called by either temp_handler or hum_handler. We accumulate until
    both halves are present, then log + publish atomically. A half older
    than 5s is dropped (in case one characteristic stopped notifying)."""
    import time as _t
    now = _t.time()
    if temp is not None: cache["temp"] = (now, int(temp))
    if hum  is not None: cache["hum"]  = (now, int(hum))
    # Drop stale halves.
    if cache["temp"] and now - cache["temp"][0] > 5.0: cache["temp"] = None
    if cache["hum"]  and now - cache["hum"][0]  > 5.0: cache["hum"]  = None
    if cache["temp"] is None or cache["hum"] is None:
        return  # not both ready yet
    ts = now
    t = cache["temp"][1]
    h = cache["hum"][1]
    cache["ts"] = ts
    line = f"{ts:.3f},{t},{h}"
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(line + "\n")
    async with state_lock:
        state["ts"] = ts
        state["temp"] = t
        state["hum"]  = h


def make_handlers():
    def temp_handler(*args, **kwargs):
        data = args[1] if len(args) > 1 else (args[0] if args else kwargs.get("data", b""))
        try:
            val = int(data[0])
        except Exception as e:
                return
        asyncio.get_event_loop().create_task(push_reading(val, None))

    def hum_handler(*args, **kwargs):
        data = args[1] if len(args) > 1 else (args[0] if args else kwargs.get("data", b""))
        try:
            val = int(data[0])
        except Exception as e:
                return
        asyncio.get_event_loop().create_task(push_reading(None, val))
    return temp_handler, hum_handler


async def ble_task():
    temp_handler, hum_handler = make_handlers()
    while True:
        print("[ble] scanning...")
        try:
            device = await BleakScanner.find_device_by_name(DEVICE_NAME, timeout=15.0)
        except Exception as e:
            print(f"[ble] scan error: {e}")
            await asyncio.sleep(5)
            continue
        if device is None:
            print("[ble] device not found, retrying in 5s")
            await asyncio.sleep(5)
            continue
        print(f"[ble] found {device.address}, connecting...")
        try:
            async with BleakClient(device) as client:
                print(f"[ble] connected: {client.is_connected}")
                async with state_lock:
                    state["connected"] = True
                last_notify = [time.time()]
                def make_wrapped(orig):
                    def w(*a, **k):
                        last_notify[0] = time.time()
                        return orig(*a, **k)
                    return w
                await client.start_notify(TEMP_CHR_UUID, make_wrapped(temp_handler))
                await client.start_notify(HUM_CHR_UUID,  make_wrapped(hum_handler))
                # Idle until notify dies for >8s OR connection drops.
                while client.is_connected:
                    await asyncio.sleep(1.0)
                    if time.time() - last_notify[0] > 8.0:
                        print("[ble] notify stalled >8s, dropping")
                        try: await client.disconnect()
                        except: pass
                        break
        except Exception as e:
            print(f"[ble] connection error: {e}")
        finally:
            async with state_lock:
                state["connected"] = False
        print("[ble] disconnected, retrying in 3s")
        await asyncio.sleep(3)


def load_history(hours):
    cutoff = time.time() - hours * 3600
    points = []
    try:
        with open(LOG_FILE, "r", encoding="utf-8") as f:
            next(f)  # skip header
            for line in f:
                parts = line.strip().split(",")
                if len(parts) != 3:
                    continue
                try:
                    ts = float(parts[0])
                    t = int(parts[1])
                    h = int(parts[2])
                except ValueError:
                    continue
                if ts >= cutoff:
                    points.append({"ts": ts, "temp": t, "hum": h})
    except FileNotFoundError:
        pass
    return points


def load_stats(hours):
    points = load_history(hours)
    if not points:
        return {"hours": hours, "count": 0}
    ts = [p["ts"] for p in points]
    temps = [p["temp"] for p in points]
    hums = [p["hum"] for p in points]
    return {
        "hours": hours,
        "count": len(points),
        "first": ts[0],
        "last":  ts[-1],
        "min_t": min(temps), "max_t": max(temps),
        "avg_t": sum(temps) / len(temps),
        "min_h": min(hums), "max_h": max(hums),
        "avg_h": sum(hums) / len(hums),
    }


def parse_hours(path):
    hours = 24
    if "?" in path:
        qs = path.split("?", 1)[1]
        for kv in qs.split("&"):
            if kv.startswith("hours="):
                try:
                    hours = max(1, min(720, int(kv[6:])))
                except ValueError:
                    pass
    return hours


async def http_handle(reader, writer):
    try:
        head = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=5)
        request_line = head.split(b"\r\n", 1)[0].decode("ascii", errors="replace")
        try:
            method, path, _ = request_line.split(" ", 2)
        except ValueError:
            writer.close()
            return
        print(f"[http] {method} {path}")
        if method != "GET":
            await respond(writer, 405, b"text/plain", b"method not allowed")
            return
        if path == "/" or path.startswith("/?"):
            body = (Path(__file__).resolve().parent / "dashboard.html").read_bytes()
            await respond(writer, 200, b"text/html; charset=utf-8", body)
        elif path == "/dashboard.html":
            body = (Path(__file__).resolve().parent / "dashboard.html").read_bytes()
            await respond(writer, 200, b"text/html; charset=utf-8", body)
        elif path.startswith("/api/current"):
            async with state_lock:
                payload = json.dumps(state).encode()
            await respond(writer, 200, b"application/json", payload)
        elif path.startswith("/api/history"):
            hours = parse_hours(path)
            payload = json.dumps({"hours": hours, "points": load_history(hours)}).encode()
            await respond(writer, 200, b"application/json", payload)
        elif path.startswith("/api/stats"):
            hours = parse_hours(path)
            payload = json.dumps(load_stats(hours)).encode()
            await respond(writer, 200, b"application/json", payload)
        else:
            await respond(writer, 404, b"text/plain", b"not found")
    except Exception as e:
        print(f"[http] handler error: {e}")
    finally:
        try:
            writer.close()
        except Exception:
            pass


async def respond(writer, status, ctype, body):
    reasons = {200: b"OK", 404: b"Not Found", 405: b"Method Not Allowed"}
    try:
        hdr = (b"HTTP/1.1 " + str(status).encode() + b" " + reasons.get(status, b"OK") + b"\r\n"
               b"Content-Type: " + ctype + b"\r\n"
               b"Content-Length: " + str(len(body)).encode() + b"\r\n"
               b"Cache-Control: no-store\r\n"
               b"Access-Control-Allow-Origin: *\r\n"
               b"Connection: close\r\n\r\n")
        writer.write(hdr + body)
        await writer.drain()
    except Exception as e:
        print(f"[http] write error: {e}")


async def main():
    asyncio.get_event_loop().create_task(ble_task())
    server = await asyncio.start_server(http_handle, "127.0.0.1", 8765)
    print("[http] dashboard at http://127.0.0.1:8765/")
    print("[http] endpoints:")
    print("        GET /api/current")
    print("        GET /api/history?hours=24")
    print("        GET /api/stats?hours=24")
    print("        GET /                      (dashboard.html)")
    async with server:
        await server.serve_forever()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nbye.")

本程序实现对应蓝牙的扫描与连接,同时开启网页服务器。

【实现效果】

在电脑脑打开网页服务器程序:

image.png

可以看到顺利的连接上了蓝牙模块。

打开网页LWX_demo - DHT11 Dashboard

image.png

可以看到连接到了温湿度计,并且有丰富的图表与历史曲线的数据展示。

对着传感器吹气,可以看到数据有实时变化:

image.png

【总结】

到此,此次e起DIY的工程就结束了,再次感谢eepw论坛,e络盟电子。

此次的的e络盟与eepw组织的活动特别好,要感谢eepw小助手,耐心的指导。感谢老师提供的优秀的教程。同时也让我在论坛中认识了许多经验丰富的老鸟。此次e起DIY活动让我学习到的Zephyr的编程知识。期待eepw多举办这么好的活动!




关键词: Zephyr     温湿度     e起DIY    

共1条 1/1 1 跳转至

回复

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