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

【实现步骤】
经过对老师的课件的以及在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.")本程序实现对应蓝牙的扫描与连接,同时开启网页服务器。
【实现效果】
在电脑脑打开网页服务器程序:

可以看到顺利的连接上了蓝牙模块。
打开网页LWX_demo - DHT11 Dashboard

可以看到连接到了温湿度计,并且有丰富的图表与历史曲线的数据展示。
对着传感器吹气,可以看到数据有实时变化:

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