Skip to content

3.10 BLE GATT 蓝牙通信 ​

这节要搞懂:手机怎么通过蓝牙给设备下发配置和 WiFi、设备怎么把状态推回手机。 需要基础:2.8、3.3

一、为什么要有蓝牙这条通道 ​

设备已经有 MQTT 可以远程控制了(3.11),为什么还要蓝牙?

因为 MQTT 必须联网,蓝牙不用。

场景MQTTBLE
设备刚拆封,还没配网❌ 用不了✅ 能用
路由器坏了 / 断网❌ 用不了✅ 能用
人在外地✅ 能用❌ 距离不够

BLE 是"没网时的兜底通道" —— 这是它在这个项目里最大的价值。它甚至可以直接给设备配 WiFi(第五节)。

所以蓝牙要早点启动

回忆 3.2 的开机顺序:app_ble_start() 排在 WiFi 连接之前。

因为它不依赖网络,早点起来,万一 WiFi 连不上还有救。

二、GATT 结构回顾 ​

概念在 2.8 讲透了,一句话复习:

服务(Service)是文件夹,特征值(Characteristic)是文件夹里的数据 —— 所有读写都是在读写特征值。

本工程用的是 NimBLE,不是 2.8 那套 Bluedroid

2.8 的入门例子用 Bluedroid(事件回调一条条走,好懂);本工程用 NimBLE,因为它省一半内存 —— 这个工程还要同时跑 WiFi、MQTT、HTTP 服务器和屏幕刷新,内存很紧张。

换的只是函数名和 GATT 的声明方式,2.8 学的概念全部照用:服务和特征值的层级、UUID、Read / Write / Notify、必须先订阅才能收到推送、MTU 协商和分包 —— 一条都没变。

所以这一节只讲这个工程定义了什么、能发什么命令,不再重复 BLE 的基础。

三、本工程的三个特征值 ⭐ ​

设备广播的名字是 TianqiClock,手机扫描时找这个名字。

📁 天气时钟服务(TianqiClock)
   ├── 📄 配置特征值        手机【写】配置 JSON,也可以【读】当前配置
   ├── 📄 状态特征值        手机【读】设备状态,或【订阅】让设备主动推
   └── 📄 WiFi 配网特征值   手机【写】ssid / password,让设备连路由器
特征值支持的操作写什么进去能干什么
配置写 / 读配置 JSON改闹钟、城市、亮度、音量、报警阈值(共 12 项,见下表)
状态读 / 订阅 Notify—看设备当前时间、温湿度、天气等
WiFi 配网写{"ssid":...,"password":...}让设备连上路由器

为什么配网要单独一个特征值

WiFi 账号密码和普通配置性质不一样:

  • 它是敏感信息,单独放便于以后加密或加权限
  • 写完之后设备要去连网、可能要重启,处理流程和改个亮度完全不同

分开更清楚,也更安全。

四、配置特征值能改什么 ⭐ ​

全部可配置项一览 ​

字段类型取值范围说明
alarm字符串"HH:MM" 或 "close"闹钟时间;close = 关闭
city字符串城市拼音天气城市
auto_light布尔true / false自动亮度开关(跟着光敏走)
light整数1 ~ 100屏幕亮度(%),关掉自动亮度时生效
auto_light_min整数1 ~ 100自动亮度下限
auto_light_max整数1 ~ 100自动亮度上限
volume整数0 ~ 100音量(%),0 = 静音
refresh_minutes整数分钟天气刷新间隔
maxtemp_set小数℃温度报警上限
minitemp_set小数℃温度报警下限
maxhum_set整数%湿度报警上限
minihum_set整数%湿度报警下限

常用命令速查表 ​

直接复制到调试工具的文本输入框就能用(工具见第十节):

想干什么写这个
设闹钟为 7 点 30 分{"alarm":"07:30"}
关闭闹钟{"alarm":"close"}
天气城市改成深圳{"city":"shenzhen"}
开启自动亮度{"auto_light":true}
关闭自动亮度,亮度固定 60%{"auto_light":false,"light":60}
自动亮度范围设为 15% ~ 90%{"auto_light_min":15,"auto_light_max":90}
音量设为 70%{"volume":70}
天气每 10 分钟刷新一次{"refresh_minutes":10}
温度报警 5℃ ~ 35℃{"maxtemp_set":35.0,"minitemp_set":5.0}
湿度报警 20% ~ 80%{"maxhum_set":80,"minihum_set":20}

一次写完整配置 ​

所有字段可以一次性发过去:

json
{
  "city": "shenzhen",
  "auto_light": true,
  "light": 80,
  "auto_light_min": 15,
  "auto_light_max": 90,
  "volume": 70,
  "alarm": "07:30",
  "refresh_minutes": 10,
  "maxtemp_set": 35.0,
  "minitemp_set": 5.0,
  "maxhum_set": 80,
  "minihum_set": 20
}

这 12 项只有 BLE 全支持

MQTT 只开放其中 8 项 —— alarm、refresh_minutes、auto_light_min、auto_light_max 目前只能用蓝牙改。

对照表见 3.11 第五节。

只写想改的字段就行

不用每次都发完整 JSON。只带上要改的字段,其他保持原样:

json
{"volume":50}

设备解析时只更新它认识且收到的字段。

"close" 就是关闭

3.7 讲过:闹钟存成字符串,判断就是一次 strcmp。

"close" 永远匹配不上任何 "HH:MM",所以天然就是"关闭",不需要额外的开关字段。

亮度是 1 ~ 100,不是 0

light、auto_light_min、auto_light_max 会被限制在 1 ~ 100,填 0 会被改成 1。

因为亮度为 0 = 屏幕全黑,用户会以为设备坏了 —— 留 1% 至少还能看见。

只有 volume 是 0 ~ 100,音量为 0 就是静音,这是合理需求。

亮度字段名认准 light

工程里为了兼容,还认这些别名:

正式字段兼容的别名
lightbrightness、ray
auto_lightauto_brightness
auto_light_minauto_brightness_min
auto_light_maxauto_brightness_max

但别用别名 —— 统一用左边那一列最清楚,也和文档、MQTT 保持一致。

refresh_minutes 别设太小

免费天气 API 有每日调用次数限制,设成 1 分钟很快就超额。10 分钟是个合适的值。

五、WiFi 配网特征值 ​

往 WiFi 配网特征值写:

想干什么写这个
连接有密码的 WiFi{"ssid":"你的WiFi名称","password":"你的WiFi密码"}
连接无密码的 WiFi{"ssid":"你的WiFi名称"}

这是第二条配网通道

3.8 讲的是网页配网(连热点 ESP32-Clock-Setup、浏览器填表)。

蓝牙配网是另一条路:不用连热点,手机连蓝牙直接写进去。

网页配网蓝牙配网
设备要变成热点吗✅ 要❌ 不用
手机要做什么连热点 + 开浏览器连蓝牙 + 写特征值
适合普通用户开发调试更快

两条都留着,各有各的场合。

六、读状态特征值 ​

手机可以主动读状态特征值,也可以订阅 Notify 让设备主动推。

返回的内容是一段设备状态文本,方便在手机上一眼看全,包含:

时间、室内温湿度、天气、城市、
自动亮度、当前亮度、光照、音量、闹钟、刷新间隔

不是标准 JSON

状态特征值返回的是给人看的文本,不是给程序解析的 JSON。

好处:用调试工具一读就懂,不用再解析。 代价:如果以后要写自己的手机 App,改成 JSON 会更好处理。

Notify 的注意点 ​

手机没订阅,推了也白推

Notify 必须手机先订阅才有效(2.8 讲过的 CCCD)。

调试时先在工具里点一下"订阅 / Enable Notification",否则你推一百次也看不到。

别推太勤

每秒推一次会明显增加功耗,手机端也处理不过来。

状态真的变了才推 —— 温度变化 0.1 度不用推。

七、数据包有多大:分包处理 ​

短 JSON 一包就够,长 JSON 会自动分包

BLE 默认的小包传输,一包大约只有 20 字节(2.8 讲过 MTU 的限制)。

{"alarm":"07:30"} 是 17 字节,一包正好装下。但第四节那个完整配置有两百多字节,一包肯定装不下。

本工程已经做了分包处理:

方向处理方式上限
手机 → 设备(配置)设备端把多个包拼接起来再解析约 512 字节
设备 → 手机(Notify)按当前 MTU 自动分包发送单包最多 244 字节

所以长 JSON 是能发的,只是它不是"一次塞进去",而是分成几包传完再处理。

学的时候先用短的

刚上手时先用 {"alarm":"07:30"} 这种短命令把流程跑通,确认能收能解析。

等短的没问题了,再试完整配置那种长 JSON —— 这样出问题时好定位是分包的锅还是解析的锅。

八、收到之后怎么处理 ​

BLE 写回调里做三件事:取数据 → 解析 JSON → 交给统一入口。

c
static int config_char_write_cb(uint16_t conn_handle, uint16_t attr_handle,
                                struct ble_gatt_access_ctxt *ctxt, void *arg)
{
    char buf[512] = {0};          // ⭐ 配置缓冲区 512 字节,容得下长 JSON
    uint16_t len = 0;

    // 1. 取出这一包数据(分包时会累积到缓冲区里)
    int rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf) - 1, &len);
    if (rc != 0 || len == 0) {
        return BLE_ATT_ERR_UNLIKELY;
    }

    // 2. 交给 JSON 解析入口
    app_config_update_from_json(buf);

    return 0;
}

app_config_update_from_json() 负责把 JSON 里认识的字段挑出来,填进配置结构体,最后调用 app_config_update() 保存并生效。

回调里别干重活

BLE 回调跑在协议栈自己的任务上。在里面刷屏、写卡、等锁,都会拖慢甚至卡死整个蓝牙。

只解析 + 更新配置,然后立刻返回(和中断服务函数是同一个原则)。

九、三类配置,各走各的链路 ⭐ ​

设备的"设置"其实分三类,不要混为一谈:

【第一类:运行配置】城市、亮度、音量、闹钟、报警阈值……

  BLE 配置特征值 ──► app_config_update_from_json() ──┐
                                                     ├──► app_config_update()
  MQTT 属性下发  ─────────────────────────────────────┘         │
                                                                ▼
                                                        写入 NVS(配置)
                                                                │
                                                                ▼
                                                          配置变化回调
                                                                │
                                                                ▼
                                                     s_config_event_queue
                                                                │
                                                                ▼
                                                       config_apply_task
                                                                │
                                                                ▼
                                                             改硬件

【第二类:WiFi 凭据】ssid / password

  HTTP 网页配网 ──► app_wifi_provision() / save_wifi() ──► NVS(wifi_cfg 命名空间)
  BLE WiFi 特征值 ─┘

【第三类:SD 卡文件操作】

  HTTP 网页 ──► app_sd / app_audio_player / app_image_viewer

别把三类混为一谈

它们走的是完全不同的链路,存的也是不同的 NVS 命名空间。

存到哪经过谁
运行配置配置命名空间app_config_update() → 配置回调 → config_apply_task
WiFi 凭据wifi_cfg 命名空间app_wifi_provision() / save_wifi()
SD 卡操作不存配置,直接操作文件app_sd 等模块

WiFi 账号密码不走 app_config_update() —— 它有自己的保存路径。混在一起讲,学员会以为改 WiFi 和改亮度是同一条链路。

但"运行配置"这一类确实汇合了

看第一类:BLE 和 MQTT 两条协议,最后都落到 app_config_update()。

BLE 多走一步 app_config_update_from_json() 解析 JSON,MQTT 在自己的回调里解析完直接调 —— 汇合之后,写 NVS、通知、应用到硬件这一整套只有一份代码。

以后再加一种控制方式(语音、红外遥控),只要它最终调到 app_config_update(),下游一行都不用动。

十、调试工具:微信小程序 ​

微信里搜小程序 「BLE 蓝牙调试助手」(同类的还有几个,名字里带 "BLE 调试" 的都能用)。

免安装,iOS 和安卓都能用 —— 扫描设备、查看服务和特征值、手动写 JSON、订阅 Notify 都能做。

调试步骤 ​

① 打开小程序,点"搜索设备"
        ↓
② 在列表里找到 TianqiClock,点连接
        ↓
③ 展开服务,能看到三个特征值
        ↓
④ 找到【配置特征值】,选"写入",
   输入框切到【文本 / UTF-8】模式,
   打 {"alarm":"07:30"} 发送
        ↓
⑤ 看设备屏幕上闹钟有没有变

输入模式一定要选"文本"

调试工具默认往往是 HEX(十六进制) 模式 —— 那样输入 {"alarm":"07:30"} 会被当成十六进制解析,设备收到的是一堆乱码。

必须切到"文本 / 字符串 / UTF-8"模式,这是新手最常卡住的地方。

它是排查问题的"标准答案"

自己写手机 App 时如果发现指令不生效,先用小程序试一遍同样的指令:

  • 小程序能改成功 → 设备端没问题,去查你的 App
  • 小程序也不行 → 问题在设备端,去看串口日志

这一步能省掉大量瞎猜的时间。

本节要点 ​

  • BLE 是没网时的兜底通道,开机流程里排在 WiFi 之前
  • 设备名 TianqiClock
  • ⭐ 三个特征值:配置(写/读)、状态(读/Notify)、WiFi 配网(写)
  • 配置用 JSON,共 12 个可配置项(第四节有一览表和速查表)
  • 只写想改的字段就行;亮度字段统一用 light
  • 闹钟 "HH:MM" 设置、"close" 关闭
  • ⭐ 配置做了分包拼接,最大约 512 字节;Notify 按 MTU 分包,单包最多 244 字节
  • 状态特征值返回的是给人看的状态文本,不是标准 JSON
  • ⭐ BLE / MQTT / 网页最终都汇合到 app_config_update()
  • 调试用微信小程序 「BLE 蓝牙调试助手」,输入模式记得切到"文本"

操作过程 ​

目标:用微信小程序连上设备,改一个配置,看屏幕跟着变。

第 1 步:准备小程序 ​

微信搜小程序 「BLE 蓝牙调试助手」,打开备用。

第 2 步:编译烧录 ​

串口应该出现:

I (1200) ble: advertising as TianqiClock

第 3 步:扫描并连接 ​

  1. 小程序点"搜索设备"
  2. 列表里找到 TianqiClock
  3. 点连接

串口:

I (30000) ble: connection established

第 4 步:找到配置特征值 ​

连上后展开服务,能看到三个特征值。找到支持"写"的那个(配置特征值)。

分不清哪个是哪个

小程序会标出每个特征值支持的操作:

  • 只有"写" → 配置特征值 或 WiFi 配网特征值
  • 有"读 + 通知" → 状态特征值

第 5 步:发第一条命令 ​

点写入,输入模式切到「文本 / UTF-8」,输入:

json
{"volume":30}

千万别用 HEX 模式

默认往往是 HEX(十六进制),那样这串 JSON 会被当成十六进制解析,设备收到的是乱码。

这是新手最常卡住的地方。

第 6 步:应该看到什么 ​

串口:

I (45000) ble: config write: {"volume":30}
I (45010) config: volume updated: 70 -> 30

第 7 步:试一条能看到现象的 ​

音量看不见,换个直观的:

json
{"auto_light":false,"light":20}

屏幕应该立刻变暗。 再发:

json
{"auto_light":false,"light":100}

屏幕变亮。 这说明整条链路通了:小程序 → BLE → 解析 → 存 NVS → 应用到硬件。

第 8 步:试试订阅状态 ​

找到状态特征值,点订阅 / Enable Notification,然后读一次 —— 应该能看到设备当前的时间、温湿度、天气等信息。

第 9 步:试试蓝牙配网 ​

往 WiFi 配网特征值写:

json
{"ssid":"你家WiFi","password":"密码"}

设备应该开始连网。

动手试试 ​

  1. 用微信小程序「BLE 蓝牙调试助手」连上 TianqiClock,找到三个特征值。
  2. 往配置特征值写 {"alarm":"07:30"},看屏幕上闹钟有没有变。
  3. 再写 {"alarm":"close"} 关掉它。
  4. 写 {"auto_light":false,"light":30},观察屏幕变暗。
  5. 订阅状态特征值,读一次设备状态。
  6. 先写短 JSON 再写第四节那个完整配置,体会分包处理的区别。
  7. 断开 WiFi,验证蓝牙依然能改配置 —— 这就是它存在的意义。