联网与数据交换 / HTTP客户端发布 内容更新

让 ESP32 请求 JSON:把超时、状态码和旧数据分清

用固定IDF客户端访问自己的局域网故障服务,逐层检查状态码、正文上限和字段协议,让失败时的最后有效读数带上年龄。

读完能做什么
本轮失败显示不可用,只有完整有效的新样本能更新最后有效值。
开始之前
已完成E01构建环境和N01联网概念,电脑能运行Python服务。
所需器材
指定S3板与USB数据线、电脑Python3、同网2.4GHz AP
版本组合
SDK:ESP-IDF5.5.5 / esp32s3协议:明文LAN HTTP;网络操作3秒;1024字节输入:自带Python3合成故障服务
适用范围

官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8;ESP-IDF5.5.5、内置HTTP/cJSON、同网电脑Python3服务;无传感器。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/networking-product.md#n03;逐项实际阅读、固定来源及限制。验证日期:
构建 / 配置已通过
docs/esp32-evidence/logs/n03-build.log;实际编译成功,摘要见networking-product-builds.json。对象:官方 ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8版本:ESP-IDF / 5.5.5验证日期:
仿真不适用
没有器件级或无线仿真;主机协议fixture与源码检查另列,不冒充ESP32仿真。
本站实板待验证
本站尚未执行这套组合的烧录、联网、传感器与实体动作;正文提供独立验收步骤。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / HTTP客户端

先约定一个能故意失败的接口

这一篇让S3主动访问你自己的局域网测试服务,拿到有单位、序号和有效性的JSON,而不是把“能访问网页”当成数据已经可用。固定IDF5.5.5、同一v1.1 N8R8板,只接USB-to-UART;不接传感器,也没有真实温度输入。

服务明确提供temperature_c数字与sequence整数,合成22.5只用于协议验收。先用同网电脑验证接口,再接S3;设备上的127.0.0.1指设备本身,不能当作电脑服务。默认192.0.2.10是文档占位,必须填写实际私有IP。

做到这里,你应该看到
说清楚服务在哪台电脑、板子访问什么实际IP,以及22.5为什么不能写成测量结果。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[2] ESP-IDF v5.5.5 HTTP客户端完整上游例程[3] ESP-IDF v5.5.5 esp_http_client官方说明[6] 乐鑫 DevKitC-1 v1.1 固定文档:修订、供电与排针

02 / HTTP客户端

同一服务准备200、404、坏JSON、超长与超时

下载包里的test_server.py只用Python标准库。先绑定127.0.0.1并用curl检查,结束本机试验后停止服务,再绑定你电脑实际私有LAN IP,并只对该实验网络开放端口。无需把服务绑定到公网地址或使用第三方账号。

/cycle按ok、404、bad、missing、slow、ok轮换,序号随请求递增。它适合一次烧录观察先成功、再失败、再恢复;/large单独验证正文上限。服务是合成故障输入,不是公网天气API,也不能用主机请求成功代替板上请求记录。

路径应提供的输入
/ok200;数字temperature_c=22.5、整数sequence
/404 /bad非200状态 / 200但无效JSON
/missing /type缺字段 / 数字被写成字符串
/large正文超过1024字节
/slow等待5秒,超过客户端3秒超时
/cycle有效→几类故障→恢复,序号递增
代码 / 配置
python3 test_server.py --bind 127.0.0.1 --port 8080
curl -i http://127.0.0.1:8080/ok
curl -i http://127.0.0.1:8080/404
curl -i http://127.0.0.1:8080/bad
curl --max-time 2 http://127.0.0.1:8080/slow
做到这里,你应该看到
主机能分别得到这些故障输入,再把同一服务开放到实际实验LAN接口。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[2] ESP-IDF v5.5.5 HTTP客户端完整上游例程[3] ESP-IDF v5.5.5 esp_http_client官方说明

03 / HTTP客户端

用E01的环境编译,Wi-Fi只决定能否发起请求

在项目根目录执行set-target和menuconfig,Demo configuration中填写2.4GHz SSID、WPA2密码和完整URL,例如你自己的http://私有IP:8080/cycle。sdkconfig已忽略,凭据不进入仓库、下载包或日志;占位凭据时不发起联网。

main/net.c使用IDF事件与2/4/8/16/30秒退避,拿到IP才设置ONLINE,断开时立即清除。HTTP在app_main中运行,连接/读取等网络操作超时配置为3000ms;它不是整次perform的总deadline,慢速分块可令总耗时更长,操作结束后再回到周期;不在Wi-Fi事件回调里解析JSON,也不在入口无限等待网络。组件清单和lock都固定5.5.5,不另装Arduino JSON库。

代码 / 配置
idf.py --version
idf.py set-target esp32s3
idf.py menuconfig
idf.py build
idf.py -p PORT flash monitor
做到这里,你应该看到
版本、target和URL明确,只有ONLINE后才出现HTTP请求;没有实板时该行为仍待验证。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[2] ESP-IDF v5.5.5 HTTP客户端完整上游例程[4] ESP-IDF v5.5.5 Wi-Fi station官方说明与事件[6] 乐鑫 DevKitC-1 v1.1 固定文档:修订、供电与排针

04 / HTTP客户端

分块正文、HTTP状态与JSON字段分别检查

timeout_ms是网络操作超时,非端到端总时限。HTTP_EVENT_ON_DATA可能分多次到来;本例逐块追加到本轮新建的1024字节缓冲。超过上限就记录BODY_TOO_LARGE,截断的JSON不会进入解析。重定向被关闭,不把一个跳转后的地址悄悄当成原接口。

perform返回ESP_OK仍可能是404,所以先检查传输,再检查200,之后才检查完整JSON对象和字段。temperature_c必须是有限数字且在-40…125°C;sequence必须为0…UINT32_MAX整数。字符串、布尔、缺字段、正文尾部其他数据都拒绝,不能用0补齐。这个范围属于本例schema,不代表任何在线服务都采用同一约定。

检查失败时
传输ESP_OKTRANSPORT_ERROR;保留错误名
HTTP200HTTP_ERROR;记录状态码
正文≤1024字节且完整JSON对象BODY_TOO_LARGE或INVALID_JSON_OR_FIELDS
单位、数值范围与整数序号不提交本轮candidate
做到这里,你应该看到
能解释为什么404和200坏JSON都不能成为新样本,而不是只看Wi-Fi图标。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[2] ESP-IDF v5.5.5 HTTP客户端完整上游例程[3] ESP-IDF v5.5.5 esp_http_client官方说明[5] cJSON v1.7.19 解析、类型检查与内存归属

05 / HTTP客户端

current与last_valid分开,旧值带年龄

每次请求先建立全新candidate;只有所有门都通过才覆盖last_valid并记录本机接收时间。失败时current=NA,另列最后有效温度、last_sequence与age_ms。服务端序号和本机接收时间用途不同,不能把age误说成服务端测量时间。

用/cycle应能观察有效样本后,HTTP/JSON/超时阶段序号保持、年龄增长,最后新的有效样本再更新。没有历史成功时输出last_valid=NONE。这样即使页面或日志保留旧数,也不会让人以为当前仍正常。

做到这里,你应该看到
失败阶段没有产生新的正常温度;旧样本有明确last标签和持续增长的年龄。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[3] ESP-IDF v5.5.5 esp_http_client官方说明[5] cJSON v1.7.19 解析、类型检查与内存归属

06 / HTTP客户端

完整主程序与可修改边界

下方是配套工程的完整main.c;网络封装、Kconfig、服务端、组件lock与默认配置都在下载包。改字段协议时一起修改服务、校验和说明,不只修改日志中的单位字符串。

本例只处理你控制的可信LAN明文HTTP。要接真实公网HTTPS服务,需要重新核对证书、时钟、认证、配额和服务协议;这份LAN测试不能证明那些条件已经满足。

代码 / 配置
// SPDX-License-Identifier: MIT
#include <string.h>
#include <math.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_http_client.h"
#include "cJSON.h"
#include "sdkconfig.h"
#include "net.h"
#define BODY_LIMIT 1024
static const char *TAG = "N03";
typedef struct { char bytes[BODY_LIMIT + 1]; size_t length; bool overflow; } body_t;
typedef struct { bool available; double temperature; uint32_t sequence; int64_t at_us; } sample_t;
static sample_t last = {0};
static esp_err_t http_event(esp_http_client_event_t *event) {
    if (event->event_id == HTTP_EVENT_ON_DATA && event->data_len > 0) {
        body_t *body = event->user_data;
        if ((size_t)event->data_len > BODY_LIMIT - body->length) body->overflow = true;
        else if (!body->overflow) {
            memcpy(body->bytes + body->length, event->data, (size_t)event->data_len);
            body->length += (size_t)event->data_len;
            body->bytes[body->length] = '\0';
        }
    }
    return ESP_OK;
}
static bool parse_sample(const body_t *body, sample_t *out) {
    const char *end = NULL;
    cJSON *root = cJSON_ParseWithLengthOpts(body->bytes, body->length + 1, &end, true);
    if (!cJSON_IsObject(root) || end != body->bytes + body->length) { cJSON_Delete(root); return false; }
    cJSON *temp = cJSON_GetObjectItemCaseSensitive(root, "temperature_c");
    cJSON *seq = cJSON_GetObjectItemCaseSensitive(root, "sequence");
    bool valid = cJSON_IsNumber(temp) && isfinite(temp->valuedouble) && temp->valuedouble >= -40 && temp->valuedouble <= 125 &&
                 cJSON_IsNumber(seq) && isfinite(seq->valuedouble) && seq->valuedouble >= 0 && seq->valuedouble <= UINT32_MAX && floor(seq->valuedouble) == seq->valuedouble;
    if (valid) *out = (sample_t){.available = true, .temperature = temp->valuedouble, .sequence = (uint32_t)seq->valuedouble, .at_us = esp_timer_get_time()};
    cJSON_Delete(root); return valid;
}
static void report_last(const char *state) {
    if (!last.available) ESP_LOGW(TAG, "state=%s current=NA last_valid=NONE", state);
    else ESP_LOGW(TAG, "state=%s current=NA last_valid_T_C=%.2f last_sequence=%" PRIu32 " age_ms=%" PRIi64, state, last.temperature, last.sequence, (esp_timer_get_time() - last.at_us) / 1000);
}
void app_main(void) {
    net_start();
    ESP_LOGI(TAG, "IDF=%s HTTP timeout=3000ms max_body=%u; no real network result recorded by this source", esp_get_idf_version(), BODY_LIMIT);
    for (;;) {
        if (!net_online()) { report_last("OFFLINE"); vTaskDelay(pdMS_TO_TICKS(5000)); continue; }
        body_t body = {0};
        esp_http_client_config_t config = {.url = CONFIG_DEMO_HTTP_URL, .timeout_ms = 3000, .event_handler = http_event, .user_data = &body, .disable_auto_redirect = true, .keep_alive_enable = false};
        esp_http_client_handle_t client = esp_http_client_init(&config);
        if (!client) { report_last("CLIENT_ALLOC_FAILED"); vTaskDelay(pdMS_TO_TICKS(5000)); continue; }
        esp_http_client_set_header(client, "Accept", "application/json");
        esp_err_t result = esp_http_client_perform(client);
        int status = esp_http_client_get_status_code(client);
        sample_t candidate = {0};
        if (result != ESP_OK) { ESP_LOGW(TAG, "transport=%s status=%d", esp_err_to_name(result), status); report_last("TRANSPORT_ERROR"); }
        else if (status != 200) { ESP_LOGW(TAG, "HTTP status=%d; body is not a sample", status); report_last("HTTP_ERROR"); }
        else if (body.overflow) report_last("BODY_TOO_LARGE");
        else if (!parse_sample(&body, &candidate)) report_last("INVALID_JSON_OR_FIELDS");
        else {
            last = candidate;
            ESP_LOGI(TAG, "state=VALID T_C=%.2f sequence=%" PRIu32 " received_ms=%" PRIi64, last.temperature, last.sequence, last.at_us / 1000);
        }
        esp_http_client_cleanup(client);
        vTaskDelay(pdMS_TO_TICKS(5000));
    }
}

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[2] ESP-IDF v5.5.5 HTTP客户端完整上游例程[3] ESP-IDF v5.5.5 esp_http_client官方说明[5] cJSON v1.7.19 解析、类型检查与内存归属

07 / HTTP客户端

用故障输入定位层级,再保留实际证据

完整观察/cycle一轮,再指向/large:记录真实状态、错误名、序号与年龄。始终OFFLINE先查凭据与事件原因;ONLINE而传输失败,先从同网电脑访问同一个私有IP和端口,再查监听接口、隔离或防火墙;HTTP404优先核对路径,200字段失败优先核对schema。

本站已实际编译客户端,并在本机逐个检查fixture的状态、正文和超时输入;没有本站ESP32访问服务、AP断开或串口运行结果。observations.csv留空,所有实板行为保持待验收。退出时Ctrl+]结束监视、Ctrl+C停服务并取消临时开放端口,回到下一篇MQTT前先确认这个数据有效性模型。

做到这里,你应该看到
每项记录能追溯到主机或ESP32实际执行端,未执行步骤没有被填成成功。

参考:[1] ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件[3] ESP-IDF v5.5.5 esp_http_client官方说明[4] ESP-IDF v5.5.5 Wi-Fi station官方说明与事件[5] cJSON v1.7.19 解析、类型检查与内存归属

BACK TO THE SOURCE

资料来源与版本说明

按指定板级修订、框架与上游源码制作。真实构建、主机辅助检查与实板行为分别记录;没有把主机测试或示意图写成本站硬件结果。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. ESP-IDF v5.5.5 esp_http_client公开接口、超时与事件
  2. ESP-IDF v5.5.5 HTTP客户端完整上游例程
  3. ESP-IDF v5.5.5 esp_http_client官方说明
  4. ESP-IDF v5.5.5 Wi-Fi station官方说明与事件
  5. cJSON v1.7.19 解析、类型检查与内存归属
  6. 乐鑫 DevKitC-1 v1.1 固定文档:修订、供电与排针