蓝牙网页与本地存储

追踪BLE队列和HTTP请求到业务状态、NVS/SPIFFS/SD,区分收到、保存与刷新,并记录分包和缓存未闭环边界。

版本与验证范围

静态源码/电路核对;用户历史反馈单独注明;本次未编译、烧录或测量

返回 00-项目导航 · 前置阅读 05-联网数据与人工智能

对象是 weixue 完整应用,不能与 moqian 驱动演示混为一谈。本篇只整理代码证据和学习练习,没有修改原工程、导出配置、构建烧录或硬件联调。源码行号以 2026-10-25 核对结果为准。

一、先认识两条控制通道

蓝牙 GATT 与设备内置 HTTP 服务是两个入口。蓝牙侧通过写特征接收 JSON,通过通知特征返回状态;HTTP 侧通过 URI 接收手机或电脑的请求。二者最后都操作设备的课程、备忘录、页面等业务状态。

ble_manager.c:187 注册一个服务和两个特征;写特征允许 WRITE、WRITE_NO_RSP,通知特征允许 NOTIFY。手机连接成功还不够,必须订阅通知,ble_is_connected() 在 :80 同时检查连接和订阅状态。设备不会因为看见名字就自动获得可靠的双向应用协议。

内置网页由 web_server_start()(web_server.c:569)在 80 端口启动。main.c:999 不以外网连接成功为前提启动它;接入点与 DNS 门户为本地访问提供入口,但这不等于所有手机都会自动弹出页面,也不等于设备已经连上互联网。

二、蓝牙回调为什么只负责入队

NimBLE 有自己的协议栈执行上下文,项目在 GATT 写回调中只做短操作,业务逻辑转交主循环:

流程示意 按原 Mermaid 线性链重绘
  1. 手机写特征
  2. 写回调复制数据
  3. 四槽接收队列
  4. 主循环取消息
  5. 解析并按命令类型分发
  6. 修改状态与设置刷新标志
  7. 主循环刷新屏幕
查看原始 Mermaid 源码
mermaid
flowchart LR
    A[手机写特征] --> B[写回调复制数据]
    B --> C[四槽接收队列]
    C --> D[主循环取消息]
    D --> E[解析并按命令类型分发]
    E --> F[修改状态与设置刷新标志]
    F --> G[主循环刷新屏幕]

证据依次是 chr_rx_access_cb()(ble_manager.c:156)、ble_process_pending()(:497)、main.c:1099、on_ble_rx()(main.c:829)、ble_cmd_handle()(ble_cmd.c:573)。这避免在蓝牙回调里直接等待墨水屏刷新或执行 HTTP。ESP-IDF 的 nimble_port_freertos_init() 默认启动协议栈线程,主循环与协议栈并非同一执行流。ESP-IDF 5.1.2 NimBLE 文档

接收队列只有四项,每项数组 512 字节(ble_manager.c:70)。当前最多复制 511 字节并补零,满队列时直接丢弃;回调仍返回零。因此,“蓝牙写操作成功”不能证明命令已经执行,更不能证明已写入 Flash。协议应该依靠业务应答确认最终状态。

三、分包有三层,不能互相替代

第一层是 BLE 协议栈传输,第二层是应用命令边界,第三层才是图片文件的分块。当前固件把一次写回调当成一个完整 JSON,没有跨写操作累积字符串的重组器。如果手机把一个 JSON 随意拆成两次写入,设备会尝试分别解析,通常都失败。

此外,写回调直接读取 ctxt->om->om_len 和第一段 om_data,没有展开整个 mbuf 链;不能把队列容量 512 当成任何情况下都可完整收到的单包能力。sdkconfig:556 的首选 ATT MTU 为 256,但首选值不等于最终协商结果。发包长度应根据协商 MTU和协议开销决定。

发送侧 ble_notify()(ble_manager.c:466)把整段 JSON 交给 ble_gatts_notify_custom(),没有应用层切片、序号或重发;长新闻列表不能据此保证一次通知完整送达。GATT 通知 API 和缓冲区所有权应按 Apache NimBLE 官方参考核对,不能用“底层会分片”替代应用协议设计。

图片协议已经存在 img_start → img_chunk → img_end(ble_cmd.c:438 起)。每块是带 Base64 文本的独立 JSON,解码后 fwrite() 到 SPIFFS,这属于文件层分块。当前 idx 只检查是否为数字,没有验证顺序或去重;声明总长度没有在结束时核对,也未检查实际写入长度、校验和或上传超时,不能称为可靠断点续传。

教学命令可以从最短消息开始,下面三条应分别作为完整消息发送,且不含任何真实配置:

json
{"type":"ping"}
{"type":"get_status"}
{"type":"refresh_news"}

其中刷新命令在 ble_cmd.c:256 只设置标志,主循环才停止蓝牙并执行联网更新。连接中断是这个路径的预期代码行为,完成后仍需要客户端重新建立连接与订阅。

四、HTTP 请求也必须循环接收

web_server.c:586 起注册首页、新闻、备忘录、无线网络、课程、状态、切页等接口。以 POST /api/memos 为例,:296 限制正文不超过 4096 字节,申请长度加一的缓冲区,循环 httpd_req_recv(),补结束符后解析 JSON。TCP 不保证一次读取就是完整正文。

下面只展示该流程的教学骨架,省略错误响应与释放分支;实际代码必须处理它们:

c
size_t received = 0;
while (received < req->content_len) {
    int n = httpd_req_recv(req, buf + received,
                           req->content_len - received);
    if (n <= 0) return ESP_FAIL; /* 实际实现应先释放 buf。 */
    received += (size_t)n;
}
buf[received] = '\0';

当前实现遇到非正返回值就失败,没有专门回复超时状态;ESP-IDF 提供 HTTPD_SOCK_ERR_TIMEOUT 供应用区分。HTTP 服务有独立任务,URI 处理器也不是主循环的一部分。ESP-IDF 5.1.2 HTTP 服务端文档

五、从“收到”到“掉电不丢”有几个状态

网页备忘录是一个很适合练习读代码的反例。api_memos_post() 在 web_server.c:334 直接更新全局数组,:353 设置 g_memo_dirty=true,随后返回成功。然而当前 main 源码中没有找到消费这个标志的逻辑。注释写着“主循环保存并刷新”,但这条链尚未接完:只能确认网页请求更新了 RAM,不能确认已经写 NVS 或刷新屏幕。

蓝牙 h_memo_update() 则会调用 storage_save_memos(),课程网页接口也直接调用保存函数,不能把不同入口的能力合并描述。更进一步,storage_save_memos() 和 storage_save_courses() 拒绝数量为零;清空列表时,即使 RAM 变空,也可能保留旧的持久数据。重启后出现旧数据,是应设计验收用例检查的边界。

并发也没有因为用了 volatile 就自动解决:HTTP 任务可能正在改数组,主循环同时读取并绘制。当前没有看到保护这些共享业务数组的统一互斥或快照机制。可改进为“HTTP/BLE 入队,主循环统一应用、保存并应答”;这是建议方向,本轮没有实施。现有蓝牙队列只隔离了自己的接收路径。

六、四种存放位置各管什么

位置 数据与入口 需要理解的区别
RAM 全局新闻、天气、课程、备忘录与显示列表 速度快,复位后不能依靠它恢复
NVS 设置、课程、备忘录、原始新闻缓存 键值与 blob;必须提交并检查错误
SPIFFS /spiffs,蓝牙上传图片 板载 Flash 文件区;不依赖插卡
SD 卡 FAT 文件系统 /sdcard,字体、书籍、日志、图片及新闻 JSON 外部存储;挂载成功后才可访问

NVS 的典型过程是 nvs_open → nvs_set_blob → nvs_commit → nvs_close。storage_manager.c:29、:62、:139 都采用类似流程,但保存函数没有把提交失败完整向上传递;“调用过保存”还不能当作“确认落盘”。把 C 结构体直接存为 blob,还意味着以后改变结构体布局时需要考虑版本兼容。ESP-IDF 5.1.2 NVS 文档

storage_manager_init()(:22)仅打印就绪,实际 nvs_flash_init() 在 main.c 启动处完成。读函数名不能代替检查初始化内容。SPIFFS 在 main.c:874 挂载到 /spiffs,配置为挂载失败可格式化;SD 在 sd_card.c:24 设置挂载失败不格式化,两者的恢复代价不同。SPIFFS 写入也不是固定耗时操作。ESP-IDF 5.1.2 SPIFFS 文档

sd_card_init() 使用 SPI2 与独立片选,调用 esp_vfs_fat_sdspi_mount() 后,VFS 才能把 fopen("/sdcard/...") 转交给 FAT 文件系统。当前主循环会重试挂载;这不能证明任意热插拔、电气连接和文件完整性均已验证。ESP-IDF 5.1.2 FAT 文档

七、缓存优先级还藏着一个实际问题

storage_save_news_sd()(storage_manager.c:172)优先存 SD JSON,打不开文件才退回 NVS;storage_load_news_sd()(:211)优先读 SD,再回退 NVS。但编排每轮直接更新 NVS,而 main.c:1070 的 SD 保存只位于启动流程。周期刷新以后,SD 旧缓存可能优先遮住更新的 NVS 缓存。

读取函数还存在边界:只要打开了 SD 文件,即使内容超长、分配失败或 JSON 解析失败,当前路径仍可能返回 ESP_OK;数组字段的声明数量也没有完全按实际解析数量规范化。因此不能宣称已经具备可靠的“损坏文件自动回退”。这是静态代码发现,需要后续用副本和设备复现,不能冒充已发生的现场故障。

八、能验证理解的练习

  1. 只读连通检查:在已有设备条件下发送 ping、订阅通知并读取状态;分别记录连接、订阅、入队和业务应答,不能只记“手机显示连接成功”。本轮未实际执行。
  2. 拆包推演:把一个 JSON 分成两次写操作,再与两个完整 img_chunk 比较;验收标准是能解释命令重组与文件分块的区别。
  3. 掉电恢复用例设计:分别用网页和蓝牙修改一条虚构备忘录,再设计清空、重启检查;验收标准是提前指出两个入口的保存差异和零条数据边界,不用真实个人数据试错。
  4. 缓存故障用例设计:使用备份卡的副本准备合法、空、截断三种新闻 JSON;验收标准是每种结果都区分 RAM、SD 和 NVS 来源,记录返回值与有效位。不要在原卡或真实缓存上直接做破坏性实验。

学完后应能把一条命令追踪为“传输接收、解析通过、状态更新、写入提交、屏幕刷新”五个独立事实,并说明哪些环节当前已接通、哪些仍只是注释中的意图。

来源与关联索引
来源
嵌入式比赛
原笔记
项目/墨嵌墨水屏/知识讲解/06-蓝牙网页与本地存储.md
资料性质
实践指南。原文的版本、来源与验证边界继续保留。
关联笔记 3
返回文章目录

想去哪里?

22 个入口

首页作品作品展厅全部项目文章总览文件夹标签资料归档关于认识 Lan探索路线工具与工作台最近在做InkFeed · 4.2 英寸电子纸信息终端VIBRATION · 柔性振动检测与采集链路TRACE · 航向保持与灰度循迹小车STRIDE · 八自由度四足机器人SCOPE · 固件日志浏览器温度为什么被 AI 改了?FIFO 有半帧,就一定是故障吗?直线守航向,弧线再看黑线。

Tab 选择入口 · Enter 打开 · Esc 关闭