返回 00-项目导航 · 配套阅读 06-蓝牙网页与本地存储
本篇分析
weixue完整终端应用。moqian是另一个驱动演示工程,不能把这里的联网和人工智能能力算到那个演示工程上。行号以核对当天源码为准;没有修改原工程,没有实际请求收费接口、重新编译、烧录或验证硬件。
一、先把“取数据”和“生成内容”分开
这个项目最值得学习的地方,是让微控制器做联网、解析和显示,让远端服务提供数据或推理。ESP32-S3 上没有在这条调用链中加载大语言模型、训练参数或进行本地大模型推理。deepseek_fetch_all() 通过 HTTPS 请求云端 DeepSeek;它还调用天气和赛事服务,因此函数名容易造成误解。
| 内容 | 当前真实来源 | ESP32-S3 做什么 |
|---|---|---|
| 原始热点标题 | baidu_fetch_hot()、weibo_fetch_hot() |
接收响应,提取标题,整理分类 |
| 天气数值 | weather_fetch_openmeteo() |
解析 Open-Meteo 的天气代码与温度数组 |
| NBA 比赛 | nba_cdn_fetch() |
解析 CDN 返回的结构化赛况 |
| 天气摘要、热点筛选 | do_deepseek_request() |
发提示词,解析模型返回内容 |
因此,提示词中的“传感器数据”只是文字描述;实际天气数据来自网络接口。weather_7d_t 虽然带有 7d,当前 URL 请求的是今日加未来三日的四天数据,不能据名字宣称七日天气已经实现。对应证据是 deepseek_fetch.c:162、:168 和 :276。
二、沿着真实调用链读一次
启动时,main.c:1033 调用 data_fetch_orchestrate(..., true, false):允许模型请求,跳过本次 NBA 请求。data_fetch.c:129 内部的顺序是:
- 清空本轮新闻结构
- 百度热点
- 微博热点
- 过滤标点、去重、生成显示列表
- 收集最多十条原始标题
- 按条件获取赛事
- 获取天气
- 请求云端摘要与筛选
- 把人工智能热点放到列表前部
- 保存原始新闻缓存
查看原始 Mermaid 源码
flowchart TD
A[清空本轮新闻结构] --> B[百度热点]
B --> C[微博热点]
C --> D[过滤标点、去重、生成显示列表]
D --> E[收集最多十条原始标题]
E --> F[按条件获取赛事]
F --> G[获取天气]
G --> H[请求云端摘要与筛选]
H --> I[把人工智能热点放到列表前部]
I --> J[保存原始新闻缓存]
build_flat_news_inline() 在 data_fetch.c:56 把分类新闻变成供界面轮播的一维数组;data_fetch_collect_raw_hot() 在 :83 提取给模型的候选标题;:97 用 memmove() 为置顶热点腾位置。这里是一条串行流水线,并没有同时请求多个网站。
要特别注意:include_deepseek=false 会跳过整个 deepseek_fetch_all(),所以连其中的天气与赛事也跳过。它并不是“保留天气,只关闭模型”的独立开关。另一个 data_fetch_hot_only() 也只处理百度和微博,不能代替天气更新。
三、哪些操作真正会触发云端请求
| 触发条件 | 代码入口 | 当前行为 |
|---|---|---|
| 开机且联网 | main.c:1033 |
获取热点、天气和模型结果;跳过 NBA |
| 无线网络恢复 | main.c:1253 |
停止蓝牙,更新全部数据,再恢复蓝牙 |
| 确认键刷新或蓝牙刷新命令 | main.c:1281、ble_cmd.c:256 |
设置标志后,由主循环执行全部更新 |
| 周期更新 | main.c:1314 |
调用相同编排;该分支没有包裹停止蓝牙逻辑 |
| 进入主界面后延迟获取赛事 | main.c:1115 |
只调用 nba_cdn_fetch(),不请求模型 |
TICK_RSS=24000 与主循环每次延时 50 ms 对应名义 20 分钟,见 main.c:1092。但这是循环计数,遇到同步 HTTP 或屏幕刷新阻塞就会延后,不能当作准确的墙上时钟定时器。注释“后台拉 NBA,不阻塞”也不能按字面理解::1120 是主循环直接调用同步函数,没有创建独立任务。
四、HTTP 分片为什么不能直接交给 JSON 解析
网络传来的是字节流,一次 esp_http_client_read() 可能只读到 JSON 的前半截,甚至停在一个汉字的 UTF-8 编码中间。应先按实际字节数拼接,最后补 \0,再进行完整解析。
do_deepseek_request() 在 deepseek_fetch.c:555 使用 init → open → write → fetch_headers → read → close → cleanup。:603 为响应分配缓冲区,已知且较小的 Content-Length 使用长度加一,否则最多分配 16384 字节;:608 循环累积读取。微博上限是 65536 字节,天气缓冲区是 4096 字节,均须为字符串结尾预留一字节。
HTTP 的 Transfer-Encoding: chunked 与“应用多次读取”不是同一层概念。ESP-IDF 负责处理 HTTP 分块编码;应用仍需反复取出有效正文。fetch_headers() 返回零可能表示没有长度头或使用分块编码,不能直接判作空正文。ESP-IDF 5.1.2 HTTP 客户端文档
当前读循环遇到 r<=0 就退出,没有区分正常结束、读取失败和等待超时,也没有用完整性检查确认正文收齐;达到容量上限时会截断。:589 的发送逻辑只检查负返回值,也没有处理写入长度小于请求长度的情况。这些是现有实现的边界,不代表已经在设备上复现故障。
下面是便于理解的读取骨架,属于教学改进示意,没有写入固件;err 必须传回调用方,且不能把整个缓冲区写满后再补结束符。
size_t used = 0;
esp_err_t err = ESP_OK;
while (used < capacity - 1) {
int n = esp_http_client_read(client, buf + used, capacity - 1 - used);
if (n < 0) { err = ESP_FAIL; break; }
if (n == 0) break;
used += (size_t)n;
}
buf[used] = '\0';
if (!esp_http_client_is_complete_data_received(client)) err = ESP_FAIL;
/* 只有 err == ESP_OK 时,才继续解析。 */
五、JSON 有两层,内存也有两种所有权
请求由 build_request_json()(deepseek_fetch.c:90)构造,加入 model 和 messages 后序列化。返回值是堆上的字符串,发送完成后释放;不能返回函数内局部数组的地址。
响应第一层是服务端协议对象,deepseek_fetch.c:674 先解析它;:683 定位 choices[0].message.content。其中 content 是字符串,字符串内部才是项目约定的第二层 JSON,例如:
{"weather":{"today_summary":"出门留意天气"},"hot":[1,3,5]}
:693 再次 cJSON_Parse() 才能提取天气与热点。对象成员指针依附于 JSON 树;需要长期保存的文字用 snprintf() 复制到结构体,最后分别释放内层与外层树。接收缓冲区、JSON 树和最终业务结构体不是同一份内存,释放顺序必须明确。
百度模块值得单独比较:baidu_fetch.c:95 起用 strstr() 搜索 JSON 文本中的固定片段,而微博模块用 cJSON 查找数组和字段。前者对空格、转义和接口格式变化更敏感,不能把两者概括成“都已通过标准 JSON 解析”。
六、约束模型输出,比提示词许愿更重要
build_prompt()(deepseek_fetch.c:36)提供真实天气与原始标题,要求模型返回热点序号。parse_hot()(:386)把从一开始的序号映射回原始标题,可以减少模型改写新闻带来的失真。不过 :401 仍兼容字符串标题,所以“绝不接收模型新写标题”还不是强制约束。
parse_weather()(:335)在 Open-Meteo 有效时保留其温度,只更新摘要;网络天气失败时,代码也允许有限范围的模型温度作为后备。这个后备温度只有范围检查,没有新的气象事实来源,答辩时应明确这一局限。
deepseek_fetch_all() 在模型请求失败而天气或赛事仍有效时也可能返回 ESP_OK(:661)。因此 g_ds_valid=true 表示编排接受了部分可用数据,不能单独证明模型调用成功。验证应分别观察天气有效位、热点数量、赛事数量及请求错误,而非只看一个“成功”标志。
七、数据生命周期、并发与验收练习
本轮新闻先被清空,再被各来源填入,然后转换成显示数组,最后保存 NVS;天气、模型热点和赛事主要保存在各自全局结构体。storage_save_news_cache() 保存的是 news_data_t,不包含后来置顶到显示数组中的模型热点,也不等于保存了完整天气、赛事和模型状态。联网失败时保留旧内容的策略尚不完整:新一轮编排先清空 RAM,不能承诺总能继续显示上一轮数据。
超时值也需要按调用链理解:热点和天气配置 15000 ms,赛事配置 8000 ms,模型配置 15000 ms;部分来源还有重试。各阶段串行叠加,整轮更新不能承诺在 15 秒内完成。主循环阻塞期间,蓝牙队列不能及时被消费,网页任务却可能仍读写共享状态,见 06-蓝牙网页与本地存储。
可按下列顺序验收,全部使用虚构数据,避免调用收费接口或展示真实配置:
- 画调用链:找出四个编排调用点,写出两个布尔参数;验收标准是能解释开机为什么没有赛事请求。
- 手工拆包:把上述内层 JSON 按任意三个字节分段拼接;验收标准是完整拼接后能解析,任取中间片段不能被当成一条业务消息。
- 检查降级:推演“天气成功、模型失败”“热点成功、天气和赛事都失败”两种路径;验收标准是能指出总返回值与各字段有效性不是同一概念。
- 拟定设备测量表:记录每个来源耗时、HTTP 状态、实际接收长度、剩余堆和屏幕响应延迟;验收标准是用测量确认超时与内存边界。这一步尚需真实设备执行。
复习时应能回答:云端模型究竟生成了什么?一次成功的 HTTP 状态为什么还不能证明 JSON 完整、业务正确或缓存已保存?
来源与关联索引
- 来源
- 嵌入式比赛
- 原笔记
- 项目/墨嵌墨水屏/知识讲解/05-联网数据与人工智能.md
- 资料性质
- 实践指南。原文的版本、来源与验证边界继续保留。