From f9cbcc396b86d61b63e0f1729b5067dbf1cce6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Sun, 2 Aug 2026 17:37:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(spi):=20=E5=A2=9E=E5=BC=BA=20SPI=20master?= =?UTF-8?q?=EF=BC=9A=E6=80=A7=E8=83=BD=E5=89=96=E6=9E=90=E4=B8=8E=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=80=97=E6=97=B6=E8=B7=9F=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `lastWorkerExecUs()`:获取最近一次 worker 任务执行耗时(µs)。 - 引入 `SpiProfile` 结构体:事务级详细剖析,记录 SPI 事务各阶段的时间戳。 - 实现 `lastProfile()`:诊断用,获取最近一次事务剖析快照。 - SPI 时钟配置提升至 13.3 MHz,改善性能。 - 修改前端 JavaScript 轮询逻辑:新增 uptime 检查,更好地处理 ESP32 重启状态。 - SPI 测速 HTML 的计时单位由毫秒改为微秒显示。 - 新建 `spi_ring_cache.h`:共享 SPI ring push 缓存工具。 - 新增 `http_util.h`:共享 HTTP 处理器工具,含 JSON 拼接与查询参数解析。 - 引入 `json_parse.h`:轻量级 JSON 解析器,针对扁平 JSON 对象设计。 - 新增 Windows 环境下的项目构建批处理脚本。 - 重构 CAN 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。 --- ...ss_a3047185-7ab8-4573-993b-07e65f6fed29.md | 81 + components/cjson/CMakeLists.txt | 8 - components/cjson/cJSON.c | 3206 ----------------- components/cjson/cJSON.h | 306 -- main/CMakeLists.txt | 1 - main/core/main.cpp | 4 +- main/include/version.h | 4 +- main/monitor/can_feedback_task.cpp | 117 +- main/monitor/can_feedback_task.h | 26 +- main/monitor/cpu_stats.h | 8 +- main/monitor/debug_log.cpp | 12 +- main/monitor/gd32_adc_feedback.cpp | 48 +- main/monitor/health_monitor.cpp | 58 +- main/monitor/health_monitor.h | 7 +- main/monitor/motor_ota_relay.cpp | 220 +- main/monitor/motor_state.cpp | 35 +- main/monitor/motor_version.cpp | 25 +- main/monitor/spi_ring_cache.h | 62 + main/monitor/spi_speed_test.cpp | 101 +- main/ota/esp32_ota_core.cpp | 11 + main/ota/ota_coordinator.cpp | 119 +- main/ota/ota_coordinator.h | 13 +- main/relay/ota_relay.cpp | 146 +- main/relay/ota_relay.h | 53 +- main/server/ble_ota.cpp | 6 +- main/server/ble_ota_data.cpp | 177 +- main/server/ble_ota_host.cpp | 3 +- main/server/ble_ota_priv.h | 3 + main/server/http_handlers.cpp | 740 ++-- main/server/http_util.h | 109 + main/server/json_parse.h | 243 ++ main/server/wifi_ota.cpp | 11 +- main/storage/gd32_flash_store.cpp | 62 +- main/storage/gd32_flash_store.h | 6 + main/transport/canfd_bridge.cpp | 69 +- main/transport/canfd_bridge.h | 18 +- main/transport/spi_master.cpp | 252 +- main/transport/spi_master.h | 69 +- sdkconfig.defaults | 8 +- tools/build_mem.bat | 23 + tools/js/common/polling.js | 18 +- tools/spi_test.html | 6 +- 42 files changed, 1957 insertions(+), 4537 deletions(-) create mode 100644 .zcode/plans/plan-sess_a3047185-7ab8-4573-993b-07e65f6fed29.md delete mode 100644 components/cjson/CMakeLists.txt delete mode 100644 components/cjson/cJSON.c delete mode 100644 components/cjson/cJSON.h create mode 100644 main/monitor/spi_ring_cache.h create mode 100644 main/server/http_util.h create mode 100644 main/server/json_parse.h create mode 100644 tools/build_mem.bat diff --git a/.zcode/plans/plan-sess_a3047185-7ab8-4573-993b-07e65f6fed29.md b/.zcode/plans/plan-sess_a3047185-7ab8-4573-993b-07e65f6fed29.md new file mode 100644 index 0000000..b5c6782 --- /dev/null +++ b/.zcode/plans/plan-sess_a3047185-7ab8-4573-993b-07e65f6fed29.md @@ -0,0 +1,81 @@ +## 方案:/can/feedback 改为请求驱动架构 + +### 目标 +- ESP32 不再被动缓存 GD32 的 0xC1 推送,改为前端请求时主动向 GD32 查询 +- GD32 停止主动推送 0xC1(降低 SPI 资源占用) +- 删除 `s_latest[3]`/`s_work[3]` 常驻 BSS(省 ~7.8KB)+ handler 栈上 `results[3]`(省 ~3.9KB 栈) + +### 依据 +- GD32 `handleFeedback` 已支持 0xC1 请求查询(`[channel, page]` → ACK + 页数据),无需新增协议 +- ESP32 `transact(0xC1, ...)` 是现成请求/响应机制,`canFdSend`/`canFdSetRxMode` 已在用 +- `canFdParseFeedbackPage` 可直接复用(只解析 payload 字节,不关心来源) +- 0xC4/0xC5/0xC6/0xD2 推送独立,不受影响 + +### 改动清单 + +#### 1. ESP32 侧 — `main/transport/canfd_bridge.cpp` / `.h` +新增 `canFdQueryFeedback(channel, page, can_feedback_result_t *out, uint8_t *entryIdx)`: +```cpp +int canFdQueryFeedback(uint8_t channel, uint8_t page, + can_feedback_result_t *out, uint8_t *entryIdx); +``` +- 发 `transact(0xC1, {channel, page}, ...)`,超时 200ms +- 成功(resp_type==ACK)→ 调 `canFdParseFeedbackPage(resp_buf, resp_len, out, entryIdx)` +- 返回 0=成功,-1=SPI/参数失败,-2=GD32 拒绝(ERROR) + +#### 2. ESP32 侧 — `main/server/http_handlers.cpp` `can_feedback_handler` +- 删除 `results[CAN_CHANNEL_MAX]` 栈数组 + `getLatestCanFeedback` 循环 +- 改为:对每个使能通道,调 `canFdQueryFeedback(ch, 0, &result, &entryIdx)` + `canFdQueryFeedback(ch, 1, &result, &entryIdx)`,在栈上用单个 `can_feedback_result_t result`(1.3KB,用完即释放) +- lite 路径:只需 page 0(统计字段在 page0 header),1 次 SPI 查询即可 +- full 路径:page 0 + page 1,2 次查询,然后序列化 +- 查询失败 → 该通道 `valid=false`(与原来无快照时行为一致) +- `pauseCanFeedbackTask` 调用保留(send_batch 仍需独占 SPI) + +#### 3. ESP32 侧 — `can_mode_handler` GET +- 原来 `getLatestCanFeedback(0, &fb)` 读缓存 mode → 改为 `canFdQueryFeedback(0, 0, &fb, &entryIdx)` 取 page0 的 mode 字段 +- 原来的 fallback `canFdSetRxMode` 保留(查询也失败时兜底) + +#### 4. ESP32 侧 — `main/monitor/can_feedback_task.cpp` +- **删除** `s_latest[3]`、`s_valid[3]`、`s_work[3]`、`s_workIdx[3]`、`s_lastLoggedError[3]`、`publish()`、`getLatestCanFeedback()` +- **删除** taskLoop 里的 0xC1 分支(`canFdParseFeedbackPage` + `publish`) +- **保留** 0xC4/0xC5/0xC6/0xD2 事件分发(这些仍走推送) +- **保留** `pauseCanFeedbackTask`(send_batch / OTA 仍需暂停事件消费) +- `s_mutex` 删除(仅保护 s_latest/s_valid,不再需要) + +#### 5. ESP32 侧 — `main/monitor/can_feedback_task.h` +- 删除 `getLatestCanFeedback` 声明 +- 保留 `startCanFeedbackTask`、`pauseCanFeedbackTask` + +#### 6. GD32 侧 — `app_spi_cmd_can_feedback.cpp` +- `canFeedbackStartRound()`:改为空函数(或加 `s_pushEnabled=false` 永久禁用推送) +- `canFeedbackPushStep()`:开头加 `if (!s_pushEnabled) return;` 或直接 return +- **保留** `handleFeedback`(请求查询处理程序不动) +- **保留** `buildFeedbackPayload`(查询和推送共用) + +#### 7. GD32 侧 — `task_runner.cpp` +- `canFeedbackStartRound()` 调用可保留(变成空函数无副作用)或删除 + +### 内存收益 +| 项 | 改前 | 改后 | 省 | +|---|---|---|---| +| `s_latest[3]` BSS | 3.9KB | 0 | 3.9KB | +| `s_work[3]` BSS | 3.9KB | 0 | 3.9KB | +| handler 栈 `results[3]` | 3.9KB | 1.3KB(单通道) | 2.6KB 栈 | +| **合计常驻 BSS** | | | **~7.8KB** | + +### 性能影响 +- 原来读缓存:<1ms +- 改后查询:lite 路径 1 次 SPI 往返 ~2-4ms;full 路径 6 次往返(3ch×2page)~12-24ms +- 前端 1Hz 轮询,24ms 完全可接受(httpd 单线程最坏阻塞 <50ms) +- GD32 不再 1Hz 推送 6 帧,省下 SPI 带宽和 PB6 中断 + +### 风险与缓解 +- **SPI 查询失败**:GD32 不响应时 `transact` 超时返回,handler 返回 valid=false,前端显示"--"(与原无快照行为一致) +- **OTA 期间查询**:send_batch/OTA 已 `pauseCanFeedbackTask`,且 SPI worker 串行化,查询不会与 OTA 数据流冲突 +- **can_monitor.html full 路径延迟**:6 次 SPI 查询 ~24ms + JSON 序列化,总 <50ms,远小于 1s 轮询间隔 + +### 不改动的部分 +- 0xC4 ADC、0xC5 motor state、0xC6 motor version、0xD2 motor OTA 推送机制不变 +- `canFdParseFeedbackPage` 解析逻辑不变 +- GD32 `handleFeedback` 查询处理程序不变 +- 前端 JS 不变(API 响应格式不变) \ No newline at end of file diff --git a/components/cjson/CMakeLists.txt b/components/cjson/CMakeLists.txt deleted file mode 100644 index 4cac17e..0000000 --- a/components/cjson/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -idf_component_register( - SRCS "cJSON.c" - INCLUDE_DIRS "." - REQUIRES -) - -# ESP-IDF uses no DLL, export all symbols -target_compile_definitions(${COMPONENT_LIB} PUBLIC CJSON_HIDE_SYMBOLS=0) diff --git a/components/cjson/cJSON.c b/components/cjson/cJSON.c deleted file mode 100644 index 88c2d95..0000000 --- a/components/cjson/cJSON.c +++ /dev/null @@ -1,3206 +0,0 @@ -/* - Copyright (c) 2009-2017 Dave Gamble and cJSON contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -/* cJSON */ -/* JSON parser in C. */ - -/* disable warnings about old C89 functions in MSVC */ -#if !defined(_CRT_SECURE_NO_DEPRECATE) && defined(_MSC_VER) -#define _CRT_SECURE_NO_DEPRECATE -#endif - -#ifdef __GNUC__ -#pragma GCC visibility push(default) -#endif -#if defined(_MSC_VER) -#pragma warning (push) -/* disable warning about single line comments in system headers */ -#pragma warning (disable : 4001) -#endif - -#include -#include -#include -#include -#include -#include -#include - -#ifdef ENABLE_LOCALES -#include -#endif - -#if defined(_MSC_VER) -#pragma warning (pop) -#endif -#ifdef __GNUC__ -#pragma GCC visibility pop -#endif - -#include "cJSON.h" - -/* define our own boolean type */ -#ifdef true -#undef true -#endif -#define true ((cJSON_bool)1) - -#ifdef false -#undef false -#endif -#define false ((cJSON_bool)0) - -/* define isnan and isinf for ANSI C, if in C99 or above, isnan and isinf has been defined in math.h */ -#ifndef isinf -#define isinf(d) (isnan((d - d)) && !isnan(d)) -#endif -#ifndef isnan -#define isnan(d) (d != d) -#endif - -#ifndef NAN -#ifdef _WIN32 -#define NAN sqrt(-1.0) -#else -#define NAN 0.0/0.0 -#endif -#endif - -typedef struct { - const unsigned char *json; - size_t position; -} error; -static error global_error = { NULL, 0 }; - -CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void) -{ - return (const char*) (global_error.json + global_error.position); -} - -CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item) -{ - if (!cJSON_IsString(item)) - { - return NULL; - } - - return item->valuestring; -} - -CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item) -{ - if (!cJSON_IsNumber(item)) - { - return (double) NAN; - } - - return item->valuedouble; -} - -/* This is a safeguard to prevent copy-pasters from using incompatible C and header files */ -#if (CJSON_VERSION_MAJOR != 1) || (CJSON_VERSION_MINOR != 7) || (CJSON_VERSION_PATCH != 19) - #error cJSON.h and cJSON.c have different versions. Make sure that both have the same. -#endif - -CJSON_PUBLIC(const char*) cJSON_Version(void) -{ - static char version[15]; - sprintf(version, "%i.%i.%i", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH); - - return version; -} - -/* Case insensitive string comparison, doesn't consider two NULL pointers equal though */ -static int case_insensitive_strcmp(const unsigned char *string1, const unsigned char *string2) -{ - if ((string1 == NULL) || (string2 == NULL)) - { - return 1; - } - - if (string1 == string2) - { - return 0; - } - - for(; tolower(*string1) == tolower(*string2); (void)string1++, string2++) - { - if (*string1 == '\0') - { - return 0; - } - } - - return tolower(*string1) - tolower(*string2); -} - -typedef struct internal_hooks -{ - void *(CJSON_CDECL *allocate)(size_t size); - void (CJSON_CDECL *deallocate)(void *pointer); - void *(CJSON_CDECL *reallocate)(void *pointer, size_t size); -} internal_hooks; - -#if defined(_MSC_VER) -/* work around MSVC error C2322: '...' address of dllimport '...' is not static */ -static void * CJSON_CDECL internal_malloc(size_t size) -{ - return malloc(size); -} -static void CJSON_CDECL internal_free(void *pointer) -{ - free(pointer); -} -static void * CJSON_CDECL internal_realloc(void *pointer, size_t size) -{ - return realloc(pointer, size); -} -#else -#define internal_malloc malloc -#define internal_free free -#define internal_realloc realloc -#endif - -/* strlen of character literals resolved at compile time */ -#define static_strlen(string_literal) (sizeof(string_literal) - sizeof("")) - -static internal_hooks global_hooks = { internal_malloc, internal_free, internal_realloc }; - -static unsigned char* cJSON_strdup(const unsigned char* string, const internal_hooks * const hooks) -{ - size_t length = 0; - unsigned char *copy = NULL; - - if (string == NULL) - { - return NULL; - } - - length = strlen((const char*)string) + sizeof(""); - copy = (unsigned char*)hooks->allocate(length); - if (copy == NULL) - { - return NULL; - } - memcpy(copy, string, length); - - return copy; -} - -CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks) -{ - if (hooks == NULL) - { - /* Reset hooks */ - global_hooks.allocate = malloc; - global_hooks.deallocate = free; - global_hooks.reallocate = realloc; - return; - } - - global_hooks.allocate = malloc; - if (hooks->malloc_fn != NULL) - { - global_hooks.allocate = hooks->malloc_fn; - } - - global_hooks.deallocate = free; - if (hooks->free_fn != NULL) - { - global_hooks.deallocate = hooks->free_fn; - } - - /* use realloc only if both free and malloc are used */ - global_hooks.reallocate = NULL; - if ((global_hooks.allocate == malloc) && (global_hooks.deallocate == free)) - { - global_hooks.reallocate = realloc; - } -} - -/* Internal constructor. */ -static cJSON *cJSON_New_Item(const internal_hooks * const hooks) -{ - cJSON* node = (cJSON*)hooks->allocate(sizeof(cJSON)); - if (node) - { - memset(node, '\0', sizeof(cJSON)); - } - - return node; -} - -/* Delete a cJSON structure. */ -CJSON_PUBLIC(void) cJSON_Delete(cJSON *item) -{ - cJSON *next = NULL; - while (item != NULL) - { - next = item->next; - if (!(item->type & cJSON_IsReference) && (item->child != NULL)) - { - cJSON_Delete(item->child); - } - if (!(item->type & cJSON_IsReference) && (item->valuestring != NULL)) - { - global_hooks.deallocate(item->valuestring); - item->valuestring = NULL; - } - if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) - { - global_hooks.deallocate(item->string); - item->string = NULL; - } - global_hooks.deallocate(item); - item = next; - } -} - -/* get the decimal point character of the current locale */ -static unsigned char get_decimal_point(void) -{ -#ifdef ENABLE_LOCALES - struct lconv *lconv = localeconv(); - return (unsigned char) lconv->decimal_point[0]; -#else - return '.'; -#endif -} - -typedef struct -{ - const unsigned char *content; - size_t length; - size_t offset; - size_t depth; /* How deeply nested (in arrays/objects) is the input at the current offset. */ - internal_hooks hooks; -} parse_buffer; - -/* check if the given size is left to read in a given parse buffer (starting with 1) */ -#define can_read(buffer, size) ((buffer != NULL) && (((buffer)->offset + size) <= (buffer)->length)) -/* check if the buffer can be accessed at the given index (starting with 0) */ -#define can_access_at_index(buffer, index) ((buffer != NULL) && (((buffer)->offset + index) < (buffer)->length)) -#define cannot_access_at_index(buffer, index) (!can_access_at_index(buffer, index)) -/* get a pointer to the buffer at the position */ -#define buffer_at_offset(buffer) ((buffer)->content + (buffer)->offset) - -/* Parse the input text to generate a number, and populate the result into item. */ -static cJSON_bool parse_number(cJSON * const item, parse_buffer * const input_buffer) -{ - double number = 0; - unsigned char *after_end = NULL; - unsigned char *number_c_string; - unsigned char decimal_point = get_decimal_point(); - size_t i = 0; - size_t number_string_length = 0; - cJSON_bool has_decimal_point = false; - - if ((input_buffer == NULL) || (input_buffer->content == NULL)) - { - return false; - } - - /* copy the number into a temporary buffer and replace '.' with the decimal point - * of the current locale (for strtod) - * This also takes care of '\0' not necessarily being available for marking the end of the input */ - for (i = 0; can_access_at_index(input_buffer, i); i++) - { - switch (buffer_at_offset(input_buffer)[i]) - { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '+': - case '-': - case 'e': - case 'E': - number_string_length++; - break; - - case '.': - number_string_length++; - has_decimal_point = true; - break; - - default: - goto loop_end; - } - } -loop_end: - /* malloc for temporary buffer, add 1 for '\0' */ - number_c_string = (unsigned char *) input_buffer->hooks.allocate(number_string_length + 1); - if (number_c_string == NULL) - { - return false; /* allocation failure */ - } - - memcpy(number_c_string, buffer_at_offset(input_buffer), number_string_length); - number_c_string[number_string_length] = '\0'; - - if (has_decimal_point) - { - for (i = 0; i < number_string_length; i++) - { - if (number_c_string[i] == '.') - { - /* replace '.' with the decimal point of the current locale (for strtod) */ - number_c_string[i] = decimal_point; - } - } - } - - number = strtod((const char*)number_c_string, (char**)&after_end); - if (number_c_string == after_end) - { - /* free the temporary buffer */ - input_buffer->hooks.deallocate(number_c_string); - return false; /* parse_error */ - } - - item->valuedouble = number; - - /* use saturation in case of overflow */ - if (number >= INT_MAX) - { - item->valueint = INT_MAX; - } - else if (number <= (double)INT_MIN) - { - item->valueint = INT_MIN; - } - else - { - item->valueint = (int)number; - } - - item->type = cJSON_Number; - - input_buffer->offset += (size_t)(after_end - number_c_string); - /* free the temporary buffer */ - input_buffer->hooks.deallocate(number_c_string); - return true; -} - -/* don't ask me, but the original cJSON_SetNumberValue returns an integer or double */ -CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number) -{ - if (object == NULL) - { - return (double)NAN; - } - - if (number >= INT_MAX) - { - object->valueint = INT_MAX; - } - else if (number <= (double)INT_MIN) - { - object->valueint = INT_MIN; - } - else - { - object->valueint = (int)number; - } - - return object->valuedouble = number; -} - -/* Note: when passing a NULL valuestring, cJSON_SetValuestring treats this as an error and return NULL */ -CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring) -{ - char *copy = NULL; - size_t v1_len; - size_t v2_len; - /* if object's type is not cJSON_String or is cJSON_IsReference, it should not set valuestring */ - if ((object == NULL) || !(object->type & cJSON_String) || (object->type & cJSON_IsReference)) - { - return NULL; - } - /* return NULL if the object is corrupted or valuestring is NULL */ - if (object->valuestring == NULL || valuestring == NULL) - { - return NULL; - } - - v1_len = strlen(valuestring); - v2_len = strlen(object->valuestring); - - if (v1_len <= v2_len) - { - /* strcpy does not handle overlapping string: [X1, X2] [Y1, Y2] => X2 < Y1 or Y2 < X1 */ - if (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring )) - { - return NULL; - } - strcpy(object->valuestring, valuestring); - return object->valuestring; - } - copy = (char*) cJSON_strdup((const unsigned char*)valuestring, &global_hooks); - if (copy == NULL) - { - return NULL; - } - if (object->valuestring != NULL) - { - cJSON_free(object->valuestring); - } - object->valuestring = copy; - - return copy; -} - -typedef struct -{ - unsigned char *buffer; - size_t length; - size_t offset; - size_t depth; /* current nesting depth (for formatted printing) */ - cJSON_bool noalloc; - cJSON_bool format; /* is this print a formatted print */ - internal_hooks hooks; -} printbuffer; - -/* realloc printbuffer if necessary to have at least "needed" bytes more */ -static unsigned char* ensure(printbuffer * const p, size_t needed) -{ - unsigned char *newbuffer = NULL; - size_t newsize = 0; - - if ((p == NULL) || (p->buffer == NULL)) - { - return NULL; - } - - if ((p->length > 0) && (p->offset >= p->length)) - { - /* make sure that offset is valid */ - return NULL; - } - - if (needed > INT_MAX) - { - /* sizes bigger than INT_MAX are currently not supported */ - return NULL; - } - - needed += p->offset + 1; - if (needed <= p->length) - { - return p->buffer + p->offset; - } - - if (p->noalloc) { - return NULL; - } - - /* calculate new buffer size */ - if (needed > (INT_MAX / 2)) - { - /* overflow of int, use INT_MAX if possible */ - if (needed <= INT_MAX) - { - newsize = INT_MAX; - } - else - { - return NULL; - } - } - else - { - newsize = needed * 2; - } - - if (p->hooks.reallocate != NULL) - { - /* reallocate with realloc if available */ - newbuffer = (unsigned char*)p->hooks.reallocate(p->buffer, newsize); - if (newbuffer == NULL) - { - p->hooks.deallocate(p->buffer); - p->length = 0; - p->buffer = NULL; - - return NULL; - } - } - else - { - /* otherwise reallocate manually */ - newbuffer = (unsigned char*)p->hooks.allocate(newsize); - if (!newbuffer) - { - p->hooks.deallocate(p->buffer); - p->length = 0; - p->buffer = NULL; - - return NULL; - } - - memcpy(newbuffer, p->buffer, p->offset + 1); - p->hooks.deallocate(p->buffer); - } - p->length = newsize; - p->buffer = newbuffer; - - return newbuffer + p->offset; -} - -/* calculate the new length of the string in a printbuffer and update the offset */ -static void update_offset(printbuffer * const buffer) -{ - const unsigned char *buffer_pointer = NULL; - if ((buffer == NULL) || (buffer->buffer == NULL)) - { - return; - } - buffer_pointer = buffer->buffer + buffer->offset; - - buffer->offset += strlen((const char*)buffer_pointer); -} - -/* securely comparison of floating-point variables */ -static cJSON_bool compare_double(double a, double b) -{ - double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b); - return (fabs(a - b) <= maxVal * DBL_EPSILON); -} - -/* Render the number nicely from the given item into a string. */ -static cJSON_bool print_number(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - double d = item->valuedouble; - int length = 0; - size_t i = 0; - unsigned char number_buffer[26] = {0}; /* temporary buffer to print the number into */ - unsigned char decimal_point = get_decimal_point(); - double test = 0.0; - - if (output_buffer == NULL) - { - return false; - } - - /* This checks for NaN and Infinity */ - if (isnan(d) || isinf(d)) - { - length = sprintf((char*)number_buffer, "null"); - } - else if(d == (double)item->valueint) - { - length = sprintf((char*)number_buffer, "%d", item->valueint); - } - else - { - /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ - length = sprintf((char*)number_buffer, "%1.15g", d); - - /* Check whether the original double can be recovered */ - if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) - { - /* If not, print with 17 decimal places of precision */ - length = sprintf((char*)number_buffer, "%1.17g", d); - } - } - - /* sprintf failed or buffer overrun occurred */ - if ((length < 0) || (length > (int)(sizeof(number_buffer) - 1))) - { - return false; - } - - /* reserve appropriate space in the output */ - output_pointer = ensure(output_buffer, (size_t)length + sizeof("")); - if (output_pointer == NULL) - { - return false; - } - - /* copy the printed number to the output and replace locale - * dependent decimal point with '.' */ - for (i = 0; i < ((size_t)length); i++) - { - if (number_buffer[i] == decimal_point) - { - output_pointer[i] = '.'; - continue; - } - - output_pointer[i] = number_buffer[i]; - } - output_pointer[i] = '\0'; - - output_buffer->offset += (size_t)length; - - return true; -} - -/* parse 4 digit hexadecimal number */ -static unsigned parse_hex4(const unsigned char * const input) -{ - unsigned int h = 0; - size_t i = 0; - - for (i = 0; i < 4; i++) - { - /* parse digit */ - if ((input[i] >= '0') && (input[i] <= '9')) - { - h += (unsigned int) input[i] - '0'; - } - else if ((input[i] >= 'A') && (input[i] <= 'F')) - { - h += (unsigned int) 10 + input[i] - 'A'; - } - else if ((input[i] >= 'a') && (input[i] <= 'f')) - { - h += (unsigned int) 10 + input[i] - 'a'; - } - else /* invalid */ - { - return 0; - } - - if (i < 3) - { - /* shift left to make place for the next nibble */ - h = h << 4; - } - } - - return h; -} - -/* converts a UTF-16 literal to UTF-8 - * A literal can be one or two sequences of the form \uXXXX */ -static unsigned char utf16_literal_to_utf8(const unsigned char * const input_pointer, const unsigned char * const input_end, unsigned char **output_pointer) -{ - long unsigned int codepoint = 0; - unsigned int first_code = 0; - const unsigned char *first_sequence = input_pointer; - unsigned char utf8_length = 0; - unsigned char utf8_position = 0; - unsigned char sequence_length = 0; - unsigned char first_byte_mark = 0; - - if ((input_end - first_sequence) < 6) - { - /* input ends unexpectedly */ - goto fail; - } - - /* get the first utf16 sequence */ - first_code = parse_hex4(first_sequence + 2); - - /* check that the code is valid */ - if (((first_code >= 0xDC00) && (first_code <= 0xDFFF))) - { - goto fail; - } - - /* UTF16 surrogate pair */ - if ((first_code >= 0xD800) && (first_code <= 0xDBFF)) - { - const unsigned char *second_sequence = first_sequence + 6; - unsigned int second_code = 0; - sequence_length = 12; /* \uXXXX\uXXXX */ - - if ((input_end - second_sequence) < 6) - { - /* input ends unexpectedly */ - goto fail; - } - - if ((second_sequence[0] != '\\') || (second_sequence[1] != 'u')) - { - /* missing second half of the surrogate pair */ - goto fail; - } - - /* get the second utf16 sequence */ - second_code = parse_hex4(second_sequence + 2); - /* check that the code is valid */ - if ((second_code < 0xDC00) || (second_code > 0xDFFF)) - { - /* invalid second half of the surrogate pair */ - goto fail; - } - - - /* calculate the unicode codepoint from the surrogate pair */ - codepoint = 0x10000 + (((first_code & 0x3FF) << 10) | (second_code & 0x3FF)); - } - else - { - sequence_length = 6; /* \uXXXX */ - codepoint = first_code; - } - - /* encode as UTF-8 - * takes at maximum 4 bytes to encode: - * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx */ - if (codepoint < 0x80) - { - /* normal ascii, encoding 0xxxxxxx */ - utf8_length = 1; - } - else if (codepoint < 0x800) - { - /* two bytes, encoding 110xxxxx 10xxxxxx */ - utf8_length = 2; - first_byte_mark = 0xC0; /* 11000000 */ - } - else if (codepoint < 0x10000) - { - /* three bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx */ - utf8_length = 3; - first_byte_mark = 0xE0; /* 11100000 */ - } - else if (codepoint <= 0x10FFFF) - { - /* four bytes, encoding 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx */ - utf8_length = 4; - first_byte_mark = 0xF0; /* 11110000 */ - } - else - { - /* invalid unicode codepoint */ - goto fail; - } - - /* encode as utf8 */ - for (utf8_position = (unsigned char)(utf8_length - 1); utf8_position > 0; utf8_position--) - { - /* 10xxxxxx */ - (*output_pointer)[utf8_position] = (unsigned char)((codepoint | 0x80) & 0xBF); - codepoint >>= 6; - } - /* encode first byte */ - if (utf8_length > 1) - { - (*output_pointer)[0] = (unsigned char)((codepoint | first_byte_mark) & 0xFF); - } - else - { - (*output_pointer)[0] = (unsigned char)(codepoint & 0x7F); - } - - *output_pointer += utf8_length; - - return sequence_length; - -fail: - return 0; -} - -/* Parse the input text into an unescaped cinput, and populate item. */ -static cJSON_bool parse_string(cJSON * const item, parse_buffer * const input_buffer) -{ - const unsigned char *input_pointer = buffer_at_offset(input_buffer) + 1; - const unsigned char *input_end = buffer_at_offset(input_buffer) + 1; - unsigned char *output_pointer = NULL; - unsigned char *output = NULL; - - /* not a string */ - if (buffer_at_offset(input_buffer)[0] != '\"') - { - goto fail; - } - - { - /* calculate approximate size of the output (overestimate) */ - size_t allocation_length = 0; - size_t skipped_bytes = 0; - while (((size_t)(input_end - input_buffer->content) < input_buffer->length) && (*input_end != '\"')) - { - /* is escape sequence */ - if (input_end[0] == '\\') - { - if ((size_t)(input_end + 1 - input_buffer->content) >= input_buffer->length) - { - /* prevent buffer overflow when last input character is a backslash */ - goto fail; - } - skipped_bytes++; - input_end++; - } - input_end++; - } - if (((size_t)(input_end - input_buffer->content) >= input_buffer->length) || (*input_end != '\"')) - { - goto fail; /* string ended unexpectedly */ - } - - /* This is at most how much we need for the output */ - allocation_length = (size_t) (input_end - buffer_at_offset(input_buffer)) - skipped_bytes; - output = (unsigned char*)input_buffer->hooks.allocate(allocation_length + sizeof("")); - if (output == NULL) - { - goto fail; /* allocation failure */ - } - } - - output_pointer = output; - /* loop through the string literal */ - while (input_pointer < input_end) - { - if (*input_pointer != '\\') - { - *output_pointer++ = *input_pointer++; - } - /* escape sequence */ - else - { - unsigned char sequence_length = 2; - if ((input_end - input_pointer) < 1) - { - goto fail; - } - - switch (input_pointer[1]) - { - case 'b': - *output_pointer++ = '\b'; - break; - case 'f': - *output_pointer++ = '\f'; - break; - case 'n': - *output_pointer++ = '\n'; - break; - case 'r': - *output_pointer++ = '\r'; - break; - case 't': - *output_pointer++ = '\t'; - break; - case '\"': - case '\\': - case '/': - *output_pointer++ = input_pointer[1]; - break; - - /* UTF-16 literal */ - case 'u': - sequence_length = utf16_literal_to_utf8(input_pointer, input_end, &output_pointer); - if (sequence_length == 0) - { - /* failed to convert UTF16-literal to UTF-8 */ - goto fail; - } - break; - - default: - goto fail; - } - input_pointer += sequence_length; - } - } - - /* zero terminate the output */ - *output_pointer = '\0'; - - item->type = cJSON_String; - item->valuestring = (char*)output; - - input_buffer->offset = (size_t) (input_end - input_buffer->content); - input_buffer->offset++; - - return true; - -fail: - if (output != NULL) - { - input_buffer->hooks.deallocate(output); - output = NULL; - } - - if (input_pointer != NULL) - { - input_buffer->offset = (size_t)(input_pointer - input_buffer->content); - } - - return false; -} - -/* Render the cstring provided to an escaped version that can be printed. */ -static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffer * const output_buffer) -{ - const unsigned char *input_pointer = NULL; - unsigned char *output = NULL; - unsigned char *output_pointer = NULL; - size_t output_length = 0; - /* numbers of additional characters needed for escaping */ - size_t escape_characters = 0; - - if (output_buffer == NULL) - { - return false; - } - - /* empty string */ - if (input == NULL) - { - output = ensure(output_buffer, sizeof("\"\"")); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "\"\""); - - return true; - } - - /* set "flag" to 1 if something needs to be escaped */ - for (input_pointer = input; *input_pointer; input_pointer++) - { - switch (*input_pointer) - { - case '\"': - case '\\': - case '\b': - case '\f': - case '\n': - case '\r': - case '\t': - /* one character escape sequence */ - escape_characters++; - break; - default: - if (*input_pointer < 32) - { - /* UTF-16 escape sequence uXXXX */ - escape_characters += 5; - } - break; - } - } - output_length = (size_t)(input_pointer - input) + escape_characters; - - output = ensure(output_buffer, output_length + sizeof("\"\"")); - if (output == NULL) - { - return false; - } - - /* no characters have to be escaped */ - if (escape_characters == 0) - { - output[0] = '\"'; - memcpy(output + 1, input, output_length); - output[output_length + 1] = '\"'; - output[output_length + 2] = '\0'; - - return true; - } - - output[0] = '\"'; - output_pointer = output + 1; - /* copy the string */ - for (input_pointer = input; *input_pointer != '\0'; (void)input_pointer++, output_pointer++) - { - if ((*input_pointer > 31) && (*input_pointer != '\"') && (*input_pointer != '\\')) - { - /* normal character, copy */ - *output_pointer = *input_pointer; - } - else - { - /* character needs to be escaped */ - *output_pointer++ = '\\'; - switch (*input_pointer) - { - case '\\': - *output_pointer = '\\'; - break; - case '\"': - *output_pointer = '\"'; - break; - case '\b': - *output_pointer = 'b'; - break; - case '\f': - *output_pointer = 'f'; - break; - case '\n': - *output_pointer = 'n'; - break; - case '\r': - *output_pointer = 'r'; - break; - case '\t': - *output_pointer = 't'; - break; - default: - /* escape and print as unicode codepoint */ - sprintf((char*)output_pointer, "u%04x", *input_pointer); - output_pointer += 4; - break; - } - } - } - output[output_length + 1] = '\"'; - output[output_length + 2] = '\0'; - - return true; -} - -/* Invoke print_string_ptr (which is useful) on an item. */ -static cJSON_bool print_string(const cJSON * const item, printbuffer * const p) -{ - return print_string_ptr((unsigned char*)item->valuestring, p); -} - -/* Predeclare these prototypes. */ -static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer); -static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer); -static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer); -static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer); - -/* Utility to jump whitespace and cr/lf */ -static parse_buffer *buffer_skip_whitespace(parse_buffer * const buffer) -{ - if ((buffer == NULL) || (buffer->content == NULL)) - { - return NULL; - } - - if (cannot_access_at_index(buffer, 0)) - { - return buffer; - } - - while (can_access_at_index(buffer, 0) && (buffer_at_offset(buffer)[0] <= 32)) - { - buffer->offset++; - } - - if (buffer->offset == buffer->length) - { - buffer->offset--; - } - - return buffer; -} - -/* skip the UTF-8 BOM (byte order mark) if it is at the beginning of a buffer */ -static parse_buffer *skip_utf8_bom(parse_buffer * const buffer) -{ - if ((buffer == NULL) || (buffer->content == NULL) || (buffer->offset != 0)) - { - return NULL; - } - - if (can_access_at_index(buffer, 4) && (strncmp((const char*)buffer_at_offset(buffer), "\xEF\xBB\xBF", 3) == 0)) - { - buffer->offset += 3; - } - - return buffer; -} - -CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated) -{ - size_t buffer_length; - - if (NULL == value) - { - return NULL; - } - - /* Adding null character size due to require_null_terminated. */ - buffer_length = strlen(value) + sizeof(""); - - return cJSON_ParseWithLengthOpts(value, buffer_length, return_parse_end, require_null_terminated); -} - -/* Parse an object - create a new root, and populate. */ -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated) -{ - parse_buffer buffer = { 0, 0, 0, 0, { 0, 0, 0 } }; - cJSON *item = NULL; - - /* reset error position */ - global_error.json = NULL; - global_error.position = 0; - - if (value == NULL || 0 == buffer_length) - { - goto fail; - } - - buffer.content = (const unsigned char*)value; - buffer.length = buffer_length; - buffer.offset = 0; - buffer.hooks = global_hooks; - - item = cJSON_New_Item(&global_hooks); - if (item == NULL) /* memory fail */ - { - goto fail; - } - - if (!parse_value(item, buffer_skip_whitespace(skip_utf8_bom(&buffer)))) - { - /* parse failure. ep is set. */ - goto fail; - } - - /* if we require null-terminated JSON without appended garbage, skip and then check for a null terminator */ - if (require_null_terminated) - { - buffer_skip_whitespace(&buffer); - if ((buffer.offset >= buffer.length) || buffer_at_offset(&buffer)[0] != '\0') - { - goto fail; - } - } - if (return_parse_end) - { - *return_parse_end = (const char*)buffer_at_offset(&buffer); - } - - return item; - -fail: - if (item != NULL) - { - cJSON_Delete(item); - } - - if (value != NULL) - { - error local_error; - local_error.json = (const unsigned char*)value; - local_error.position = 0; - - if (buffer.offset < buffer.length) - { - local_error.position = buffer.offset; - } - else if (buffer.length > 0) - { - local_error.position = buffer.length - 1; - } - - if (return_parse_end != NULL) - { - *return_parse_end = (const char*)local_error.json + local_error.position; - } - - global_error = local_error; - } - - return NULL; -} - -/* Default options for cJSON_Parse */ -CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value) -{ - return cJSON_ParseWithOpts(value, 0, 0); -} - -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length) -{ - return cJSON_ParseWithLengthOpts(value, buffer_length, 0, 0); -} - -#define cjson_min(a, b) (((a) < (b)) ? (a) : (b)) - -static unsigned char *print(const cJSON * const item, cJSON_bool format, const internal_hooks * const hooks) -{ - static const size_t default_buffer_size = 256; - printbuffer buffer[1]; - unsigned char *printed = NULL; - - memset(buffer, 0, sizeof(buffer)); - - /* create buffer */ - buffer->buffer = (unsigned char*) hooks->allocate(default_buffer_size); - buffer->length = default_buffer_size; - buffer->format = format; - buffer->hooks = *hooks; - if (buffer->buffer == NULL) - { - goto fail; - } - - /* print the value */ - if (!print_value(item, buffer)) - { - goto fail; - } - update_offset(buffer); - - /* check if reallocate is available */ - if (hooks->reallocate != NULL) - { - printed = (unsigned char*) hooks->reallocate(buffer->buffer, buffer->offset + 1); - if (printed == NULL) { - goto fail; - } - buffer->buffer = NULL; - } - else /* otherwise copy the JSON over to a new buffer */ - { - printed = (unsigned char*) hooks->allocate(buffer->offset + 1); - if (printed == NULL) - { - goto fail; - } - memcpy(printed, buffer->buffer, cjson_min(buffer->length, buffer->offset + 1)); - printed[buffer->offset] = '\0'; /* just to be sure */ - - /* free the buffer */ - hooks->deallocate(buffer->buffer); - buffer->buffer = NULL; - } - - return printed; - -fail: - if (buffer->buffer != NULL) - { - hooks->deallocate(buffer->buffer); - buffer->buffer = NULL; - } - - if (printed != NULL) - { - hooks->deallocate(printed); - printed = NULL; - } - - return NULL; -} - -/* Render a cJSON item/entity/structure to text. */ -CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item) -{ - return (char*)print(item, true, &global_hooks); -} - -CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item) -{ - return (char*)print(item, false, &global_hooks); -} - -CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt) -{ - printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; - - if (prebuffer < 0) - { - return NULL; - } - - p.buffer = (unsigned char*)global_hooks.allocate((size_t)prebuffer); - if (!p.buffer) - { - return NULL; - } - - p.length = (size_t)prebuffer; - p.offset = 0; - p.noalloc = false; - p.format = fmt; - p.hooks = global_hooks; - - if (!print_value(item, &p)) - { - global_hooks.deallocate(p.buffer); - p.buffer = NULL; - return NULL; - } - - return (char*)p.buffer; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format) -{ - printbuffer p = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; - - if ((length < 0) || (buffer == NULL)) - { - return false; - } - - p.buffer = (unsigned char*)buffer; - p.length = (size_t)length; - p.offset = 0; - p.noalloc = true; - p.format = format; - p.hooks = global_hooks; - - return print_value(item, &p); -} - -/* Parser core - when encountering text, process appropriately. */ -static cJSON_bool parse_value(cJSON * const item, parse_buffer * const input_buffer) -{ - if ((input_buffer == NULL) || (input_buffer->content == NULL)) - { - return false; /* no input */ - } - - /* parse the different types of values */ - /* null */ - if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "null", 4) == 0)) - { - item->type = cJSON_NULL; - input_buffer->offset += 4; - return true; - } - /* false */ - if (can_read(input_buffer, 5) && (strncmp((const char*)buffer_at_offset(input_buffer), "false", 5) == 0)) - { - item->type = cJSON_False; - input_buffer->offset += 5; - return true; - } - /* true */ - if (can_read(input_buffer, 4) && (strncmp((const char*)buffer_at_offset(input_buffer), "true", 4) == 0)) - { - item->type = cJSON_True; - item->valueint = 1; - input_buffer->offset += 4; - return true; - } - /* string */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '\"')) - { - return parse_string(item, input_buffer); - } - /* number */ - if (can_access_at_index(input_buffer, 0) && ((buffer_at_offset(input_buffer)[0] == '-') || ((buffer_at_offset(input_buffer)[0] >= '0') && (buffer_at_offset(input_buffer)[0] <= '9')))) - { - return parse_number(item, input_buffer); - } - /* array */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '[')) - { - return parse_array(item, input_buffer); - } - /* object */ - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '{')) - { - return parse_object(item, input_buffer); - } - - return false; -} - -/* Render a value to text. */ -static cJSON_bool print_value(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output = NULL; - - if ((item == NULL) || (output_buffer == NULL)) - { - return false; - } - - switch ((item->type) & 0xFF) - { - case cJSON_NULL: - output = ensure(output_buffer, 5); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "null"); - return true; - - case cJSON_False: - output = ensure(output_buffer, 6); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "false"); - return true; - - case cJSON_True: - output = ensure(output_buffer, 5); - if (output == NULL) - { - return false; - } - strcpy((char*)output, "true"); - return true; - - case cJSON_Number: - return print_number(item, output_buffer); - - case cJSON_Raw: - { - size_t raw_length = 0; - if (item->valuestring == NULL) - { - return false; - } - - raw_length = strlen(item->valuestring) + sizeof(""); - output = ensure(output_buffer, raw_length); - if (output == NULL) - { - return false; - } - memcpy(output, item->valuestring, raw_length); - return true; - } - - case cJSON_String: - return print_string(item, output_buffer); - - case cJSON_Array: - return print_array(item, output_buffer); - - case cJSON_Object: - return print_object(item, output_buffer); - - default: - return false; - } -} - -/* Build an array from input text. */ -static cJSON_bool parse_array(cJSON * const item, parse_buffer * const input_buffer) -{ - cJSON *head = NULL; /* head of the linked list */ - cJSON *current_item = NULL; - - if (input_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* to deeply nested */ - } - input_buffer->depth++; - - if (buffer_at_offset(input_buffer)[0] != '[') - { - /* not an array */ - goto fail; - } - - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ']')) - { - /* empty array */ - goto success; - } - - /* check if we skipped to the end of the buffer */ - if (cannot_access_at_index(input_buffer, 0)) - { - input_buffer->offset--; - goto fail; - } - - /* step back to character in front of the first element */ - input_buffer->offset--; - /* loop through the comma separated array elements */ - do - { - /* allocate next item */ - cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); - if (new_item == NULL) - { - goto fail; /* allocation failure */ - } - - /* attach next item to list */ - if (head == NULL) - { - /* start the linked list */ - current_item = head = new_item; - } - else - { - /* add to the end and advance */ - current_item->next = new_item; - new_item->prev = current_item; - current_item = new_item; - } - - /* parse next value */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_value(current_item, input_buffer)) - { - goto fail; /* failed to parse value */ - } - buffer_skip_whitespace(input_buffer); - } - while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); - - if (cannot_access_at_index(input_buffer, 0) || buffer_at_offset(input_buffer)[0] != ']') - { - goto fail; /* expected end of array */ - } - -success: - input_buffer->depth--; - - if (head != NULL) { - head->prev = current_item; - } - - item->type = cJSON_Array; - item->child = head; - - input_buffer->offset++; - - return true; - -fail: - if (head != NULL) - { - cJSON_Delete(head); - } - - return false; -} - -/* Render an array to text */ -static cJSON_bool print_array(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - size_t length = 0; - cJSON *current_element = item->child; - - if (output_buffer == NULL) - { - return false; - } - - if (output_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* nesting is too deep */ - } - - /* Compose the output array. */ - /* opening square bracket */ - output_pointer = ensure(output_buffer, 1); - if (output_pointer == NULL) - { - return false; - } - - *output_pointer = '['; - output_buffer->offset++; - output_buffer->depth++; - - while (current_element != NULL) - { - if (!print_value(current_element, output_buffer)) - { - return false; - } - update_offset(output_buffer); - if (current_element->next) - { - length = (size_t) (output_buffer->format ? 2 : 1); - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ','; - if(output_buffer->format) - { - *output_pointer++ = ' '; - } - *output_pointer = '\0'; - output_buffer->offset += length; - } - current_element = current_element->next; - } - - output_pointer = ensure(output_buffer, 2); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ']'; - *output_pointer = '\0'; - output_buffer->depth--; - - return true; -} - -/* Build an object from the text. */ -static cJSON_bool parse_object(cJSON * const item, parse_buffer * const input_buffer) -{ - cJSON *head = NULL; /* linked list head */ - cJSON *current_item = NULL; - - if (input_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* to deeply nested */ - } - input_buffer->depth++; - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '{')) - { - goto fail; /* not an object */ - } - - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == '}')) - { - goto success; /* empty object */ - } - - /* check if we skipped to the end of the buffer */ - if (cannot_access_at_index(input_buffer, 0)) - { - input_buffer->offset--; - goto fail; - } - - /* step back to character in front of the first element */ - input_buffer->offset--; - /* loop through the comma separated array elements */ - do - { - /* allocate next item */ - cJSON *new_item = cJSON_New_Item(&(input_buffer->hooks)); - if (new_item == NULL) - { - goto fail; /* allocation failure */ - } - - /* attach next item to list */ - if (head == NULL) - { - /* start the linked list */ - current_item = head = new_item; - } - else - { - /* add to the end and advance */ - current_item->next = new_item; - new_item->prev = current_item; - current_item = new_item; - } - - if (cannot_access_at_index(input_buffer, 1)) - { - goto fail; /* nothing comes after the comma */ - } - - /* parse the name of the child */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_string(current_item, input_buffer)) - { - goto fail; /* failed to parse name */ - } - buffer_skip_whitespace(input_buffer); - - /* swap valuestring and string, because we parsed the name */ - current_item->string = current_item->valuestring; - current_item->valuestring = NULL; - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != ':')) - { - goto fail; /* invalid object */ - } - - /* parse the value */ - input_buffer->offset++; - buffer_skip_whitespace(input_buffer); - if (!parse_value(current_item, input_buffer)) - { - goto fail; /* failed to parse value */ - } - buffer_skip_whitespace(input_buffer); - } - while (can_access_at_index(input_buffer, 0) && (buffer_at_offset(input_buffer)[0] == ',')); - - if (cannot_access_at_index(input_buffer, 0) || (buffer_at_offset(input_buffer)[0] != '}')) - { - goto fail; /* expected end of object */ - } - -success: - input_buffer->depth--; - - if (head != NULL) { - head->prev = current_item; - } - - item->type = cJSON_Object; - item->child = head; - - input_buffer->offset++; - return true; - -fail: - if (head != NULL) - { - cJSON_Delete(head); - } - - return false; -} - -/* Render an object to text. */ -static cJSON_bool print_object(const cJSON * const item, printbuffer * const output_buffer) -{ - unsigned char *output_pointer = NULL; - size_t length = 0; - cJSON *current_item = item->child; - - if (output_buffer == NULL) - { - return false; - } - - if (output_buffer->depth >= CJSON_NESTING_LIMIT) - { - return false; /* nesting is too deep */ - } - - /* Compose the output: */ - length = (size_t) (output_buffer->format ? 2 : 1); /* fmt: {\n */ - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - - *output_pointer++ = '{'; - output_buffer->depth++; - if (output_buffer->format) - { - *output_pointer++ = '\n'; - } - output_buffer->offset += length; - - while (current_item) - { - if (output_buffer->format) - { - size_t i; - output_pointer = ensure(output_buffer, output_buffer->depth); - if (output_pointer == NULL) - { - return false; - } - for (i = 0; i < output_buffer->depth; i++) - { - *output_pointer++ = '\t'; - } - output_buffer->offset += output_buffer->depth; - } - - /* print key */ - if (!print_string_ptr((unsigned char*)current_item->string, output_buffer)) - { - return false; - } - update_offset(output_buffer); - - length = (size_t) (output_buffer->format ? 2 : 1); - output_pointer = ensure(output_buffer, length); - if (output_pointer == NULL) - { - return false; - } - *output_pointer++ = ':'; - if (output_buffer->format) - { - *output_pointer++ = '\t'; - } - output_buffer->offset += length; - - /* print value */ - if (!print_value(current_item, output_buffer)) - { - return false; - } - update_offset(output_buffer); - - /* print comma if not last */ - length = ((size_t)(output_buffer->format ? 1 : 0) + (size_t)(current_item->next ? 1 : 0)); - output_pointer = ensure(output_buffer, length + 1); - if (output_pointer == NULL) - { - return false; - } - if (current_item->next) - { - *output_pointer++ = ','; - } - - if (output_buffer->format) - { - *output_pointer++ = '\n'; - } - *output_pointer = '\0'; - output_buffer->offset += length; - - current_item = current_item->next; - } - - output_pointer = ensure(output_buffer, output_buffer->format ? (output_buffer->depth + 1) : 2); - if (output_pointer == NULL) - { - return false; - } - if (output_buffer->format) - { - size_t i; - for (i = 0; i < (output_buffer->depth - 1); i++) - { - *output_pointer++ = '\t'; - } - } - *output_pointer++ = '}'; - *output_pointer = '\0'; - output_buffer->depth--; - - return true; -} - -/* Get Array size/item / object item. */ -CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array) -{ - cJSON *child = NULL; - size_t size = 0; - - if (array == NULL) - { - return 0; - } - - child = array->child; - - while(child != NULL) - { - size++; - child = child->next; - } - - /* FIXME: Can overflow here. Cannot be fixed without breaking the API */ - - return (int)size; -} - -static cJSON* get_array_item(const cJSON *array, size_t index) -{ - cJSON *current_child = NULL; - - if (array == NULL) - { - return NULL; - } - - current_child = array->child; - while ((current_child != NULL) && (index > 0)) - { - index--; - current_child = current_child->next; - } - - return current_child; -} - -CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index) -{ - if (index < 0) - { - return NULL; - } - - return get_array_item(array, (size_t)index); -} - -static cJSON *get_object_item(const cJSON * const object, const char * const name, const cJSON_bool case_sensitive) -{ - cJSON *current_element = NULL; - - if ((object == NULL) || (name == NULL)) - { - return NULL; - } - - current_element = object->child; - if (case_sensitive) - { - while ((current_element != NULL) && (current_element->string != NULL) && (strcmp(name, current_element->string) != 0)) - { - current_element = current_element->next; - } - } - else - { - while ((current_element != NULL) && (case_insensitive_strcmp((const unsigned char*)name, (const unsigned char*)(current_element->string)) != 0)) - { - current_element = current_element->next; - } - } - - if ((current_element == NULL) || (current_element->string == NULL)) { - return NULL; - } - - return current_element; -} - -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) -{ - return get_object_item(object, string, false); -} - -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string) -{ - return get_object_item(object, string, true); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string) -{ - return cJSON_GetObjectItem(object, string) ? 1 : 0; -} - -/* Utility for array list handling. */ -static void suffix_object(cJSON *prev, cJSON *item) -{ - prev->next = item; - item->prev = prev; -} - -/* Utility for handling references. */ -static cJSON *create_reference(const cJSON *item, const internal_hooks * const hooks) -{ - cJSON *reference = NULL; - if (item == NULL) - { - return NULL; - } - - reference = cJSON_New_Item(hooks); - if (reference == NULL) - { - return NULL; - } - - memcpy(reference, item, sizeof(cJSON)); - reference->string = NULL; - reference->type |= cJSON_IsReference; - reference->next = reference->prev = NULL; - return reference; -} - -static cJSON_bool add_item_to_array(cJSON *array, cJSON *item) -{ - cJSON *child = NULL; - - if ((item == NULL) || (array == NULL) || (array == item)) - { - return false; - } - - child = array->child; - /* - * To find the last item in array quickly, we use prev in array - */ - if (child == NULL) - { - /* list is empty, start new one */ - array->child = item; - item->prev = item; - item->next = NULL; - } - else - { - /* append to the end */ - if (child->prev) - { - suffix_object(child->prev, item); - array->child->prev = item; - } - } - - return true; -} - -/* Add item to array/object. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item) -{ - return add_item_to_array(array, item); -} - -#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) - #pragma GCC diagnostic push -#endif -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wcast-qual" -#endif -/* helper function to cast away const */ -static void* cast_away_const(const void* string) -{ - return (void*)string; -} -#if defined(__clang__) || (defined(__GNUC__) && ((__GNUC__ > 4) || ((__GNUC__ == 4) && (__GNUC_MINOR__ > 5)))) - #pragma GCC diagnostic pop -#endif - - -static cJSON_bool add_item_to_object(cJSON * const object, const char * const string, cJSON * const item, const internal_hooks * const hooks, const cJSON_bool constant_key) -{ - char *new_key = NULL; - int new_type = cJSON_Invalid; - - if ((object == NULL) || (string == NULL) || (item == NULL) || (object == item)) - { - return false; - } - - if (constant_key) - { - new_key = (char*)cast_away_const(string); - new_type = item->type | cJSON_StringIsConst; - } - else - { - new_key = (char*)cJSON_strdup((const unsigned char*)string, hooks); - if (new_key == NULL) - { - return false; - } - - new_type = item->type & ~cJSON_StringIsConst; - } - - if (!(item->type & cJSON_StringIsConst) && (item->string != NULL)) - { - hooks->deallocate(item->string); - } - - item->string = new_key; - item->type = new_type; - - return add_item_to_array(object, item); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item) -{ - return add_item_to_object(object, string, item, &global_hooks, false); -} - -/* Add an item to an object with constant string as key */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item) -{ - return add_item_to_object(object, string, item, &global_hooks, true); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item) -{ - if (array == NULL) - { - return false; - } - - return add_item_to_array(array, create_reference(item, &global_hooks)); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item) -{ - if ((object == NULL) || (string == NULL)) - { - return false; - } - - return add_item_to_object(object, string, create_reference(item, &global_hooks), &global_hooks, false); -} - -CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name) -{ - cJSON *null = cJSON_CreateNull(); - if (add_item_to_object(object, name, null, &global_hooks, false)) - { - return null; - } - - cJSON_Delete(null); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name) -{ - cJSON *true_item = cJSON_CreateTrue(); - if (add_item_to_object(object, name, true_item, &global_hooks, false)) - { - return true_item; - } - - cJSON_Delete(true_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name) -{ - cJSON *false_item = cJSON_CreateFalse(); - if (add_item_to_object(object, name, false_item, &global_hooks, false)) - { - return false_item; - } - - cJSON_Delete(false_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean) -{ - cJSON *bool_item = cJSON_CreateBool(boolean); - if (add_item_to_object(object, name, bool_item, &global_hooks, false)) - { - return bool_item; - } - - cJSON_Delete(bool_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number) -{ - cJSON *number_item = cJSON_CreateNumber(number); - if (add_item_to_object(object, name, number_item, &global_hooks, false)) - { - return number_item; - } - - cJSON_Delete(number_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) -{ - cJSON *string_item = cJSON_CreateString(string); - if (add_item_to_object(object, name, string_item, &global_hooks, false)) - { - return string_item; - } - - cJSON_Delete(string_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw) -{ - cJSON *raw_item = cJSON_CreateRaw(raw); - if (add_item_to_object(object, name, raw_item, &global_hooks, false)) - { - return raw_item; - } - - cJSON_Delete(raw_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name) -{ - cJSON *object_item = cJSON_CreateObject(); - if (add_item_to_object(object, name, object_item, &global_hooks, false)) - { - return object_item; - } - - cJSON_Delete(object_item); - return NULL; -} - -CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name) -{ - cJSON *array = cJSON_CreateArray(); - if (add_item_to_object(object, name, array, &global_hooks, false)) - { - return array; - } - - cJSON_Delete(array); - return NULL; -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item) -{ - if ((parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)) - { - return NULL; - } - - if (item != parent->child) - { - /* not the first element */ - item->prev->next = item->next; - } - if (item->next != NULL) - { - /* not the last element */ - item->next->prev = item->prev; - } - - if (item == parent->child) - { - /* first element */ - parent->child = item->next; - } - else if (item->next == NULL) - { - /* last element */ - parent->child->prev = item->prev; - } - - /* make sure the detached item doesn't point anywhere anymore */ - item->prev = NULL; - item->next = NULL; - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which) -{ - if (which < 0) - { - return NULL; - } - - return cJSON_DetachItemViaPointer(array, get_array_item(array, (size_t)which)); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which) -{ - cJSON_Delete(cJSON_DetachItemFromArray(array, which)); -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string) -{ - cJSON *to_detach = cJSON_GetObjectItem(object, string); - - return cJSON_DetachItemViaPointer(object, to_detach); -} - -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string) -{ - cJSON *to_detach = cJSON_GetObjectItemCaseSensitive(object, string); - - return cJSON_DetachItemViaPointer(object, to_detach); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string) -{ - cJSON_Delete(cJSON_DetachItemFromObject(object, string)); -} - -CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string) -{ - cJSON_Delete(cJSON_DetachItemFromObjectCaseSensitive(object, string)); -} - -/* Replace array/object items with new ones. */ -CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem) -{ - cJSON *after_inserted = NULL; - - if (which < 0 || newitem == NULL) - { - return false; - } - - after_inserted = get_array_item(array, (size_t)which); - if (after_inserted == NULL) - { - return add_item_to_array(array, newitem); - } - - if (after_inserted != array->child && after_inserted->prev == NULL) { - /* return false if after_inserted is a corrupted array item */ - return false; - } - - newitem->next = after_inserted; - newitem->prev = after_inserted->prev; - after_inserted->prev = newitem; - if (after_inserted == array->child) - { - array->child = newitem; - } - else - { - newitem->prev->next = newitem; - } - return true; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement) -{ - if ((parent == NULL) || (parent->child == NULL) || (replacement == NULL) || (item == NULL)) - { - return false; - } - - if (replacement == item) - { - return true; - } - - replacement->next = item->next; - replacement->prev = item->prev; - - if (replacement->next != NULL) - { - replacement->next->prev = replacement; - } - if (parent->child == item) - { - if (parent->child->prev == parent->child) - { - replacement->prev = replacement; - } - parent->child = replacement; - } - else - { /* - * To find the last item in array quickly, we use prev in array. - * We can't modify the last item's next pointer where this item was the parent's child - */ - if (replacement->prev != NULL) - { - replacement->prev->next = replacement; - } - if (replacement->next == NULL) - { - parent->child->prev = replacement; - } - } - - item->next = NULL; - item->prev = NULL; - cJSON_Delete(item); - - return true; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem) -{ - if (which < 0) - { - return false; - } - - return cJSON_ReplaceItemViaPointer(array, get_array_item(array, (size_t)which), newitem); -} - -static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive) -{ - if ((replacement == NULL) || (string == NULL)) - { - return false; - } - - /* replace the name in the replacement */ - if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL)) - { - cJSON_free(replacement->string); - } - replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); - if (replacement->string == NULL) - { - return false; - } - - replacement->type &= ~cJSON_StringIsConst; - - return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem) -{ - return replace_item_in_object(object, string, newitem, false); -} - -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object, const char *string, cJSON *newitem) -{ - return replace_item_in_object(object, string, newitem, true); -} - -/* Create basic types: */ -CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_NULL; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_True; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_False; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = boolean ? cJSON_True : cJSON_False; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_Number; - item->valuedouble = num; - - /* use saturation in case of overflow */ - if (num >= INT_MAX) - { - item->valueint = INT_MAX; - } - else if (num <= (double)INT_MIN) - { - item->valueint = INT_MIN; - } - else - { - item->valueint = (int)num; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_String; - item->valuestring = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks); - if(!item->valuestring) - { - cJSON_Delete(item); - return NULL; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) - { - item->type = cJSON_String | cJSON_IsReference; - item->valuestring = (char*)cast_away_const(string); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) { - item->type = cJSON_Object | cJSON_IsReference; - item->child = (cJSON*)cast_away_const(child); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child) { - cJSON *item = cJSON_New_Item(&global_hooks); - if (item != NULL) { - item->type = cJSON_Array | cJSON_IsReference; - item->child = (cJSON*)cast_away_const(child); - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type = cJSON_Raw; - item->valuestring = (char*)cJSON_strdup((const unsigned char*)raw, &global_hooks); - if(!item->valuestring) - { - cJSON_Delete(item); - return NULL; - } - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if(item) - { - item->type=cJSON_Array; - } - - return item; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void) -{ - cJSON *item = cJSON_New_Item(&global_hooks); - if (item) - { - item->type = cJSON_Object; - } - - return item; -} - -/* Create Arrays: */ -CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber(numbers[i]); - if (!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber((double)numbers[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (numbers == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for(i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateNumber(numbers[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p, n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count) -{ - size_t i = 0; - cJSON *n = NULL; - cJSON *p = NULL; - cJSON *a = NULL; - - if ((count < 0) || (strings == NULL)) - { - return NULL; - } - - a = cJSON_CreateArray(); - - for (i = 0; a && (i < (size_t)count); i++) - { - n = cJSON_CreateString(strings[i]); - if(!n) - { - cJSON_Delete(a); - return NULL; - } - if(!i) - { - a->child = n; - } - else - { - suffix_object(p,n); - } - p = n; - } - - if (a && a->child) { - a->child->prev = n; - } - - return a; -} - -/* Duplication */ -cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse); - -CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse) -{ - return cJSON_Duplicate_rec(item, 0, recurse ); -} - -cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recurse) -{ - cJSON *newitem = NULL; - cJSON *child = NULL; - cJSON *next = NULL; - cJSON *newchild = NULL; - - /* Bail on bad ptr */ - if (!item) - { - goto fail; - } - /* Create new item */ - newitem = cJSON_New_Item(&global_hooks); - if (!newitem) - { - goto fail; - } - /* Copy over all vars */ - newitem->type = item->type & (~cJSON_IsReference); - newitem->valueint = item->valueint; - newitem->valuedouble = item->valuedouble; - if (item->valuestring) - { - newitem->valuestring = (char*)cJSON_strdup((unsigned char*)item->valuestring, &global_hooks); - if (!newitem->valuestring) - { - goto fail; - } - } - if (item->string) - { - newitem->string = (item->type&cJSON_StringIsConst) ? item->string : (char*)cJSON_strdup((unsigned char*)item->string, &global_hooks); - if (!newitem->string) - { - goto fail; - } - } - /* If non-recursive, then we're done! */ - if (!recurse) - { - return newitem; - } - /* Walk the ->next chain for the child. */ - child = item->child; - while (child != NULL) - { - if(depth >= CJSON_CIRCULAR_LIMIT) { - goto fail; - } - newchild = cJSON_Duplicate_rec(child, depth + 1, true); /* Duplicate (with recurse) each item in the ->next chain */ - if (!newchild) - { - goto fail; - } - if (next != NULL) - { - /* If newitem->child already set, then crosswire ->prev and ->next and move on */ - next->next = newchild; - newchild->prev = next; - next = newchild; - } - else - { - /* Set newitem->child and move to it */ - newitem->child = newchild; - next = newchild; - } - child = child->next; - } - if (newitem && newitem->child) - { - newitem->child->prev = newchild; - } - - return newitem; - -fail: - if (newitem != NULL) - { - cJSON_Delete(newitem); - } - - return NULL; -} - -static void skip_oneline_comment(char **input) -{ - *input += static_strlen("//"); - - for (; (*input)[0] != '\0'; ++(*input)) - { - if ((*input)[0] == '\n') { - *input += static_strlen("\n"); - return; - } - } -} - -static void skip_multiline_comment(char **input) -{ - *input += static_strlen("/*"); - - for (; (*input)[0] != '\0'; ++(*input)) - { - if (((*input)[0] == '*') && ((*input)[1] == '/')) - { - *input += static_strlen("*/"); - return; - } - } -} - -static void minify_string(char **input, char **output) { - (*output)[0] = (*input)[0]; - *input += static_strlen("\""); - *output += static_strlen("\""); - - - for (; (*input)[0] != '\0'; (void)++(*input), ++(*output)) { - (*output)[0] = (*input)[0]; - - if ((*input)[0] == '\"') { - (*output)[0] = '\"'; - *input += static_strlen("\""); - *output += static_strlen("\""); - return; - } else if (((*input)[0] == '\\') && ((*input)[1] == '\"')) { - (*output)[1] = (*input)[1]; - *input += static_strlen("\""); - *output += static_strlen("\""); - } - } -} - -CJSON_PUBLIC(void) cJSON_Minify(char *json) -{ - char *into = json; - - if (json == NULL) - { - return; - } - - while (json[0] != '\0') - { - switch (json[0]) - { - case ' ': - case '\t': - case '\r': - case '\n': - json++; - break; - - case '/': - if (json[1] == '/') - { - skip_oneline_comment(&json); - } - else if (json[1] == '*') - { - skip_multiline_comment(&json); - } else { - json++; - } - break; - - case '\"': - minify_string(&json, (char**)&into); - break; - - default: - into[0] = json[0]; - json++; - into++; - } - } - - /* and null-terminate. */ - *into = '\0'; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Invalid; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_False; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xff) == cJSON_True; -} - - -CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & (cJSON_True | cJSON_False)) != 0; -} -CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_NULL; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Number; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_String; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Array; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Object; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) -{ - if (item == NULL) - { - return false; - } - - return (item->type & 0xFF) == cJSON_Raw; -} - -CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) -{ - if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) - { - return false; - } - - /* check if type is valid */ - switch (a->type & 0xFF) - { - case cJSON_False: - case cJSON_True: - case cJSON_NULL: - case cJSON_Number: - case cJSON_String: - case cJSON_Raw: - case cJSON_Array: - case cJSON_Object: - break; - - default: - return false; - } - - /* identical objects are equal */ - if (a == b) - { - return true; - } - - switch (a->type & 0xFF) - { - /* in these cases and equal type is enough */ - case cJSON_False: - case cJSON_True: - case cJSON_NULL: - return true; - - case cJSON_Number: - if (compare_double(a->valuedouble, b->valuedouble)) - { - return true; - } - return false; - - case cJSON_String: - case cJSON_Raw: - if ((a->valuestring == NULL) || (b->valuestring == NULL)) - { - return false; - } - if (strcmp(a->valuestring, b->valuestring) == 0) - { - return true; - } - - return false; - - case cJSON_Array: - { - cJSON *a_element = a->child; - cJSON *b_element = b->child; - - for (; (a_element != NULL) && (b_element != NULL);) - { - if (!cJSON_Compare(a_element, b_element, case_sensitive)) - { - return false; - } - - a_element = a_element->next; - b_element = b_element->next; - } - - /* one of the arrays is longer than the other */ - if (a_element != b_element) { - return false; - } - - return true; - } - - case cJSON_Object: - { - cJSON *a_element = NULL; - cJSON *b_element = NULL; - cJSON_ArrayForEach(a_element, a) - { - /* TODO This has O(n^2) runtime, which is horrible! */ - b_element = get_object_item(b, a_element->string, case_sensitive); - if (b_element == NULL) - { - return false; - } - - if (!cJSON_Compare(a_element, b_element, case_sensitive)) - { - return false; - } - } - - /* doing this twice, once on a and b to prevent true comparison if a subset of b - * TODO: Do this the proper way, this is just a fix for now */ - cJSON_ArrayForEach(b_element, b) - { - a_element = get_object_item(a, b_element->string, case_sensitive); - if (a_element == NULL) - { - return false; - } - - if (!cJSON_Compare(b_element, a_element, case_sensitive)) - { - return false; - } - } - - return true; - } - - default: - return false; - } -} - -CJSON_PUBLIC(void *) cJSON_malloc(size_t size) -{ - return global_hooks.allocate(size); -} - -CJSON_PUBLIC(void) cJSON_free(void *object) -{ - global_hooks.deallocate(object); - object = NULL; -} diff --git a/components/cjson/cJSON.h b/components/cjson/cJSON.h deleted file mode 100644 index cab5feb..0000000 --- a/components/cjson/cJSON.h +++ /dev/null @@ -1,306 +0,0 @@ -/* - Copyright (c) 2009-2017 Dave Gamble and cJSON contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. -*/ - -#ifndef cJSON__h -#define cJSON__h - -#ifdef __cplusplus -extern "C" -{ -#endif - -#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32)) -#define __WINDOWS__ -#endif - -#ifdef __WINDOWS__ - -/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options: - -CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols -CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default) -CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol - -For *nix builds that support visibility attribute, you can define similar behavior by - -setting default visibility to hidden by adding --fvisibility=hidden (for gcc) -or --xldscope=hidden (for sun cc) -to CFLAGS - -then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does - -*/ - -#define CJSON_CDECL __cdecl -#define CJSON_STDCALL __stdcall - -/* export symbols by default, this is necessary for copy pasting the C and header file */ -#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS) -#define CJSON_EXPORT_SYMBOLS -#endif - -#if defined(CJSON_HIDE_SYMBOLS) -#define CJSON_PUBLIC(type) type CJSON_STDCALL -#elif defined(CJSON_EXPORT_SYMBOLS) -#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL -#elif defined(CJSON_IMPORT_SYMBOLS) -#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL -#endif -#else /* !__WINDOWS__ */ -#define CJSON_CDECL -#define CJSON_STDCALL - -#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY) -#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type -#else -#define CJSON_PUBLIC(type) type -#endif -#endif - -/* project version */ -#define CJSON_VERSION_MAJOR 1 -#define CJSON_VERSION_MINOR 7 -#define CJSON_VERSION_PATCH 19 - -#include - -/* cJSON Types: */ -#define cJSON_Invalid (0) -#define cJSON_False (1 << 0) -#define cJSON_True (1 << 1) -#define cJSON_NULL (1 << 2) -#define cJSON_Number (1 << 3) -#define cJSON_String (1 << 4) -#define cJSON_Array (1 << 5) -#define cJSON_Object (1 << 6) -#define cJSON_Raw (1 << 7) /* raw json */ - -#define cJSON_IsReference 256 -#define cJSON_StringIsConst 512 - -/* The cJSON structure: */ -typedef struct cJSON -{ - /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */ - struct cJSON *next; - struct cJSON *prev; - /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */ - struct cJSON *child; - - /* The type of the item, as above. */ - int type; - - /* The item's string, if type==cJSON_String and type == cJSON_Raw */ - char *valuestring; - /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ - int valueint; - /* The item's number, if type==cJSON_Number */ - double valuedouble; - - /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ - char *string; -} cJSON; - -typedef struct cJSON_Hooks -{ - /* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */ - void *(CJSON_CDECL *malloc_fn)(size_t sz); - void (CJSON_CDECL *free_fn)(void *ptr); -} cJSON_Hooks; - -typedef int cJSON_bool; - -/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them. - * This is to prevent stack overflows. */ -#ifndef CJSON_NESTING_LIMIT -#define CJSON_NESTING_LIMIT 1000 -#endif - -/* Limits the length of circular references can be before cJSON rejects to parse them. - * This is to prevent stack overflows. */ -#ifndef CJSON_CIRCULAR_LIMIT -#define CJSON_CIRCULAR_LIMIT 10000 -#endif - -/* returns the version of cJSON as a string */ -CJSON_PUBLIC(const char*) cJSON_Version(void); - -/* Supply malloc, realloc and free functions to cJSON */ -CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks); - -/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */ -/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */ -CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value); -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length); -/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */ -/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */ -CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated); -CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated); - -/* Render a cJSON entity to text for transfer/storage. */ -CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item); -/* Render a cJSON entity to text for transfer/storage without any formatting. */ -CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item); -/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */ -CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt); -/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */ -/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */ -CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format); -/* Delete a cJSON entity and all subentities. */ -CJSON_PUBLIC(void) cJSON_Delete(cJSON *item); - -/* Returns the number of items in an array (or object). */ -CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array); -/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */ -CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index); -/* Get item "string" from object. Case insensitive. */ -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string); -CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string); -CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string); -/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */ -CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void); - -/* Check item type and return its value */ -CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item); -CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item); - -/* These functions check the type of an item */ -CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item); -CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item); - -/* These calls create a cJSON item of the appropriate type. */ -CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean); -CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num); -CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string); -/* raw json */ -CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw); -CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void); -CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void); - -/* Create a string where valuestring references a string so - * it will not be freed by cJSON_Delete */ -CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string); -/* Create an object/array that only references it's elements so - * they will not be freed by cJSON_Delete */ -CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child); -CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child); - -/* These utilities create an Array of count items. - * The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/ -CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count); -CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count); - -/* Append item to the specified array/object. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item); -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item); -/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object. - * WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before - * writing to `item->string` */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item); -/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */ -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item); -CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item); - -/* Remove/Detach items from Arrays/Objects. */ -CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which); -CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string); -CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string); -CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string); -CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string); - -/* Update array items. */ -CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */ -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem); -CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem); - -/* Duplicate a cJSON item */ -CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse); -/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will - * need to be released. With recurse!=0, it will duplicate any children connected to the item. - * The item->next and ->prev pointers are always zero on return from Duplicate. */ -/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal. - * case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */ -CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive); - -/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings. - * The input pointer json cannot point to a read-only address area, such as a string constant, - * but should point to a readable and writable address area. */ -CJSON_PUBLIC(void) cJSON_Minify(char *json); - -/* Helper functions for creating and adding items to an object at the same time. - * They return the added item or NULL on failure. */ -CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); -CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); -CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); -CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); -CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); -CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name); - -/* When assigning an integer value, it needs to be propagated to valuedouble too. */ -#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number)) -/* helper for the cJSON_SetNumberValue macro */ -CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number); -#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number)) -/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */ -CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring); - -/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/ -#define cJSON_SetBoolValue(object, boolValue) ( \ - (object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \ - (object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \ - cJSON_Invalid\ -) - -/* Macro for iterating over an array or object */ -#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next) - -/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */ -CJSON_PUBLIC(void *) cJSON_malloc(size_t size); -CJSON_PUBLIC(void) cJSON_free(void *object); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 25c03f8..57f9063 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -35,7 +35,6 @@ idf_component_register( REQUIRES transport # M5: 传输层组件(SPI/CANFD/协议) app_update # ESP32 OTA API - cjson # JSON 解析/生成 esp_driver_tsens # 温度传感器驱动 esp_driver_uart # UART 驱动(调试日志) esp_wifi # WiFi STA/AP 模式 diff --git a/main/core/main.cpp b/main/core/main.cpp index ac4bd25..aeb6dea 100644 --- a/main/core/main.cpp +++ b/main/core/main.cpp @@ -132,7 +132,9 @@ extern "C" void app_main(void) SystemBoot::mark(BOOT_OTA_VALID); } - DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s, heap=%lu, wifi_early=%s", + /* ★ banner 文案必须包含「启动」,前端 polling.js 靠 + * /OTA Relay .*启动/ 匹配本行判定 ESP32 重启完成(阶段 6)。 */ + DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s 启动, heap=%lu, wifi_early=%s", FW_VERSION, (unsigned long)esp_get_free_heap_size(), wifi_early_started_sta ? "STA" : "AP/other"); diff --git a/main/include/version.h b/main/include/version.h index d7b689b..9484ffd 100644 --- a/main/include/version.h +++ b/main/include/version.h @@ -14,8 +14,8 @@ // 固件版本 #define FW_VERSION_MAJOR 0 #define FW_VERSION_MINOR 3 -#define FW_VERSION_PATCH 74 -#define FW_VERSION_STRING "0.3.74" // can batch: 帧间隔 5→1ms;前端 BATCH_CHUNK=64 + fetch 超时加固 + finally 兜底 renderHistory +#define FW_VERSION_PATCH 76 +#define FW_VERSION_STRING "0.3.76" // 重启恢复提速2:httpd就绪后再打 MAIN「启动」banner(防环缩容裁掉);前端用 uptime<120s 判重启完成,不再干等10s // Git 信息(手动填写) #define GIT_HASH "770ae14" \ No newline at end of file diff --git a/main/monitor/can_feedback_task.cpp b/main/monitor/can_feedback_task.cpp index 4e946e3..be91239 100644 --- a/main/monitor/can_feedback_task.cpp +++ b/main/monitor/can_feedback_task.cpp @@ -1,14 +1,11 @@ /* - * can_feedback_task.cpp - 被动监听 GD32 主动推送的 CAN 反馈快照 + * can_feedback_task.cpp - 被动监听 GD32 主动推送的事件 * - * 架构(GD32 主动推送,ESP32 被动接收): - * GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页, - * 通过 requestEsp32Read(0xC1) 下拉 DATA_READY 主动按铃,将每页反馈 payload - * 推送给 ESP32。ESP32 本任务 SpiMaster::takeEvent() 阻塞等待按铃/事件, - * 读到一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再 - * publish() 到 s_latest[channel] 供 Web 读取。 - * - * 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。 + * 架构(2026-08 改造后): + * 0xC1 CAN 反馈已改为请求驱动(前端请求时 ESP32 主动向 GD32 查询), + * 不再被动缓存。本任务仅保留 0xC4/0xC5/0xC6/0xD2 事件分发: + * 0xC4 ADC 调试反馈、0xC5 多电机状态、0xC6 多电机版本、0xD2 电机 OTA 进度 + * 这些仍由 GD32 主动推送,ESP32 被动接收。 * * OTA 期间通过 pauseCanFeedbackTask(true) 暂停本任务,避免与 OTA 抢占 SPI。 */ @@ -17,7 +14,6 @@ #include "freertos/semphr.h" #include "app_config.h" -#include "transport/canfd_bridge.h" #include "transport/spi_master.h" #include "monitor/debug_log.h" #include "monitor/can_feedback_task.h" @@ -25,7 +21,6 @@ #include "monitor/motor_state.h" #include "monitor/motor_version.h" #include "monitor/motor_ota_relay.h" -#include "app_config.h" #include "esp_system.h" #include @@ -34,7 +29,6 @@ namespace { constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时 constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔 -constexpr uint8_t kSpiCanFeedbackType = 0xC1U; constexpr uint8_t kSpiAdcFeedbackType = 0xC4U; ///< GD32 ADC 调试反馈 constexpr uint8_t kSpiMotorStateType = 0xC5U; ///< GD32 多电机状态推送 constexpr uint8_t kSpiMotorVersionType = 0xC6U; ///< GD32 多电机版本推送(事件驱动) @@ -42,44 +36,22 @@ constexpr uint8_t kSpiMotorOtaStatusType = 0xD2U; ///< GD32 电机 OTA relay constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断 constexpr const char *kTag = "CAN_FB"; -static can_feedback_result_t s_latest[CAN_CHANNEL_MAX]; -static bool s_valid [CAN_CHANNEL_MAX]; -static SemaphoreHandle_t s_mutex = nullptr; -static volatile bool s_paused = false; - -/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */ -static can_feedback_result_t s_work [CAN_CHANNEL_MAX]; -static uint8_t s_workIdx[CAN_CHANNEL_MAX]; -static uint32_t s_lastLoggedError[CAN_CHANNEL_MAX]; +static volatile bool s_paused = false; /** - * @brief 将某通道完整反馈发布到 s_latest(线程安全)。 - * @param ch 通道 0..2 - * @param fb 解析后的快照 - */ -static void publish(uint8_t ch, const can_feedback_result_t &fb) -{ - if (s_mutex == nullptr) return; - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { - s_latest[ch] = fb; - s_valid[ch] = true; - xSemaphoreGive(s_mutex); - } -} - -/** - * @brief can_fb 任务主循环:takeEvent 分发 0xC1 CAN / 0xC4 ADC。 + * @brief can_fb 任务主循环:takeEvent 分发 0xC4/0xC5/0xC6/0xD2 推送事件。 + * 0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback),不再在此处理。 */ static void taskLoop(void *) { - DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)"); + DEBUG_INFO(kTag, "事件监听任务启动 (0xC4 ADC / 0xC5 motor / 0xC6 ver / 0xD2 ota)"); /* 冷启动延后开始监听,错开上电初始化窗口 */ { esp_reset_reason_t rr = esp_reset_reason(); if (rr == ESP_RST_POWERON || rr == ESP_RST_BROWNOUT) { constexpr uint32_t kCanFbDeferMs = 1500U; - DEBUG_INFO(kTag, "冷启动: %lu ms 后开始 CAN 反馈监听", + DEBUG_INFO(kTag, "冷启动: %lu ms 后开始事件监听", (unsigned long)kCanFbDeferMs); vTaskDelay(pdMS_TO_TICKS(kCanFbDeferMs)); } @@ -126,39 +98,14 @@ static void taskLoop(void *) continue; } - if (ring_type != kSpiCanFeedbackType || ring_len < 28U) { - continue; - } - - const uint8_t ch = ring_payload[0]; - if (ch >= CAN_CHANNEL_MAX) { - continue; - } - - /* 解析本页并累积到工作缓冲,page=0 会自动重置 */ - if (canFdParseFeedbackPage(ring_payload, ring_len, - &s_work[ch], &s_workIdx[ch]) == 0) { - publish(ch, s_work[ch]); - - /* 累计错误发生变化时才告警一次;P0/P1 携带相同统计,不能重复打印。 */ - if (s_work[ch].stats.error_count != 0U - && s_work[ch].stats.error_count != s_lastLoggedError[ch]) { - DEBUG_WARN(kTag, - "CH%u err=%lu TEC=%u REC=%u", - static_cast(ch), - static_cast(s_work[ch].stats.error_count), - static_cast(s_work[ch].stats.tec), - static_cast(s_work[ch].stats.rec)); - s_lastLoggedError[ch] = s_work[ch].stats.error_count; - } - } + /* 0xC1 已改为请求驱动,不再在此处理;未知类型静默跳过 */ } } } // namespace /** - * @brief 启动 CAN 反馈被动监听任务(幂等)。 + * @brief 启动事件监听任务(幂等)。 * @return true 表示任务已存在或创建成功。 */ bool startCanFeedbackTask() @@ -166,19 +113,6 @@ bool startCanFeedbackTask() static bool s_started = false; if (s_started) return true; - if (s_mutex == nullptr) { - s_mutex = xSemaphoreCreateMutex(); - if (s_mutex == nullptr) { - DEBUG_ERR(kTag, "互斥量创建失败"); - return false; - } - } - memset(s_latest, 0, sizeof(s_latest)); - memset(s_lastLoggedError, 0, sizeof(s_lastLoggedError)); - memset(s_valid, 0, sizeof(s_valid)); - memset(s_work, 0, sizeof(s_work)); - memset(s_workIdx, 0, sizeof(s_workIdx)); - BaseType_t xRet = xTaskCreatePinnedToCore( taskLoop, "can_fb", @@ -196,7 +130,7 @@ bool startCanFeedbackTask() } /** - * @brief 暂停/恢复 CAN(及同任务分发的 ADC)事件监听。 + * @brief 暂停/恢复事件监听。 * @param pause true=OTA 期间暂停;false=恢复 */ void pauseCanFeedbackTask(bool pause) @@ -204,28 +138,17 @@ void pauseCanFeedbackTask(bool pause) if (s_paused == pause) return; // 幂等:状态未变时不重复日志 s_paused = pause; if (pause) { - DEBUG_DBG(kTag, "CAN 监听已暂停(OTA 进行中)"); + DEBUG_DBG(kTag, "事件监听已暂停(OTA 进行中)"); } else { - DEBUG_DBG(kTag, "CAN 监听已恢复"); + DEBUG_DBG(kTag, "事件监听已恢复"); } } /** - * @brief 拷贝某通道的最新快照(线程安全)。 - * @return true 表示拿到一份有效数据。 + * @brief 查询是否处于暂停(OTA / send_batch 期间为 true)。 + * 暂停期间 HTTP handler 应跳过 0xC1 查询,避免与 OTA 抢占 SPI。 */ -bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out) +bool canFeedbackPaused() { - if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) { - return false; - } - bool ok = false; - if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { - if (s_valid[channel]) { - *out = s_latest[channel]; - ok = true; - } - xSemaphoreGive(s_mutex); - } - return ok; + return s_paused; } diff --git a/main/monitor/can_feedback_task.h b/main/monitor/can_feedback_task.h index 6a03e64..0546f6a 100644 --- a/main/monitor/can_feedback_task.h +++ b/main/monitor/can_feedback_task.h @@ -1,37 +1,31 @@ /* - * can_feedback_task.h - 被动接收 GD32 主动推送的 CAN 反馈快照 + * can_feedback_task.h - 事件监听任务(0xC4/0xC5/0xC6/0xD2 推送) * - * ESP32 侧不主动 1Hz 轮询 0xC1;由 GD32 按铃推送,本任务 takeEvent 接收。 + * 2026-08 架构改造:0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback), + * 不再被动缓存。本任务仅分发 GD32 主动推送的 ADC/电机状态/版本/OTA 进度事件。 */ #ifndef CAN_FEEDBACK_TASK_H #define CAN_FEEDBACK_TASK_H -#include -#include "transport/canfd_bridge.h" - -/** @brief 暂停/恢复 CAN 反馈监听(OTA 期间调用 pause=true)。 */ +/** @brief 暂停/恢复事件监听(OTA 期间调用 pause=true)。 */ void pauseCanFeedbackTask(bool pause); +/** @brief 查询监听是否处于暂停(OTA / send_batch 期间为 true)。 + * 请求驱动的 SPI 查询(canFdQueryFeedback)在暂停期间应跳过。 */ +bool canFeedbackPaused(); + #ifdef __cplusplus extern "C" { #endif /** - * @brief 启动 CAN 反馈被动监听任务(幂等)。 + * @brief 启动事件监听任务(幂等)。 * @return true 表示任务已存在或创建成功。 */ bool startCanFeedbackTask(); -/** - * @brief 拷贝某通道的最新反馈快照(线程安全)。 - * @param channel 0/1/2。 - * @param out 输出缓冲。 - * @return true 表示拿到一份有效数据,false 表示通道无效或尚未拿到第一份。 - */ -bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out); - #ifdef __cplusplus } #endif -#endif // CAN_FEEDBACK_TASK_H +#endif // CAN_FEEDBACK_TASK_H \ No newline at end of file diff --git a/main/monitor/cpu_stats.h b/main/monitor/cpu_stats.h index 2d8eb1d..f2760a0 100644 --- a/main/monitor/cpu_stats.h +++ b/main/monitor/cpu_stats.h @@ -11,8 +11,12 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" -/** 单次快照最多跟踪的任务数 */ -static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32; +/** 单次快照最多跟踪的任务数。 + * 系统常驻任务 ~14(app)+ ~5(IDF: tiT/wifi/nimble/hci/ble)= ~19, + * 按需短暂任务(ble_finish/restart/cancel/gd32_wait/ota_feed)不常驻。 + * 20 覆盖常驻集 + 余量 1;偶发按需任务多出时 uxTaskGetSystemState 截断, + * 仅影响该拍 CPU 统计显示,不影响功能。原 32 浪费 ~1KB BSS。 */ +static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 20; /** 采样周期(ms)— 窗口 sample_ms 应对齐此值 */ static constexpr uint32_t CPU_STATS_SAMPLE_MS = 1000; diff --git a/main/monitor/debug_log.cpp b/main/monitor/debug_log.cpp index 721a96b..ddba53a 100644 --- a/main/monitor/debug_log.cpp +++ b/main/monitor/debug_log.cpp @@ -509,7 +509,12 @@ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len) return ESP_ERR_INVALID_STATE; } - const int ret = uart_write_bytes(UART_NUM_0, data, len); + /* 非阻塞直写 FIFO:满了丢弃(日志必进 ring,web /logs 仍完整)。 + * 旧实现 uart_write_bytes 在 TX buffer=0 时同步阻塞到整行发完 + * (115200 下 ~5-8ms/条),OTA 期间日志密集会把 ota_relay(prio 23) + * / spi_worker(prio 6) 阻塞数 ms~数十 ms——50ms ACK 窗口被拖爆, + * 表现为改短重试后超时重发频繁(线上 12/777)。 */ + const int ret = uart_tx_chars(UART_NUM_0, (const char *)data, (int)len); if (ret < 0) { return ESP_FAIL; } @@ -646,7 +651,10 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len) if (sock >= 0) { const size_t send_len = len > 1024 ? 1024 : len; - const int ret = ::sendto(sock, data, send_len, 0, + /* MSG_DONTWAIT:非阻塞发送。lwIP 在 ARP 未解析/路由未就绪时 + * sendto 会排队等待(可达 SO_SNDTIMEO 200ms),会阻塞打日志的 + * 业务任务(OTA relay/worker)——日志转发必须尽力而为。 */ + const int ret = ::sendto(sock, data, send_len, MSG_DONTWAIT, (struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr)); if (ret >= 0) { udp_ret = ESP_OK; diff --git a/main/monitor/gd32_adc_feedback.cpp b/main/monitor/gd32_adc_feedback.cpp index bc2dde5..2e71070 100644 --- a/main/monitor/gd32_adc_feedback.cpp +++ b/main/monitor/gd32_adc_feedback.cpp @@ -2,9 +2,7 @@ * gd32_adc_feedback.cpp - GD32 ADC 调试反馈解析与缓存 */ #include "monitor/gd32_adc_feedback.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" +#include "monitor/spi_ring_cache.h" #include @@ -17,29 +15,6 @@ static SemaphoreHandle_t s_mux = nullptr; static gd32_adc_feedback_t s_latest = {}; static bool s_have = false; -/** @brief 读小端 u16。 */ -static uint16_t rdU16Le(const uint8_t *p) -{ - return static_cast(p[0] | (static_cast(p[1]) << 8)); -} - -/** @brief 读小端 u32。 */ -static uint32_t rdU32Le(const uint8_t *p) -{ - return static_cast(p[0]) - | (static_cast(p[1]) << 8) - | (static_cast(p[2]) << 16) - | (static_cast(p[3]) << 24); -} - -/** @brief 懒创建快照互斥量。 */ -static void ensureMux() -{ - if (s_mux == nullptr) { - s_mux = xSemaphoreCreateMutex(); - } -} - } // namespace /** @@ -80,7 +55,7 @@ bool onGd32AdcFeedbackEvent(const uint8_t *payload, size_t len) fb.uptime_sec = uptime; fb.recv_ms = esp_timer_get_time() / 1000; - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) { return false; } @@ -103,7 +78,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out) if (out == nullptr) { return false; } - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) { return false; } @@ -124,7 +99,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out) */ uint32_t gd32AdcFeedbackAgeMs(void) { - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) { return UINT32_MAX; } @@ -135,16 +110,5 @@ uint32_t gd32AdcFeedbackAgeMs(void) recv = s_latest.recv_ms; xSemaphoreGive(s_mux); } - if (!have || recv <= 0) { - return UINT32_MAX; - } - const int64_t now = esp_timer_get_time() / 1000; - if (now < recv) { - return 0U; - } - const int64_t age = now - recv; - if (age > static_cast(UINT32_MAX)) { - return UINT32_MAX; - } - return static_cast(age); -} + return ringCacheAgeMs(have, recv); +} \ No newline at end of file diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp index 1a8d286..74a0509 100644 --- a/main/monitor/health_monitor.cpp +++ b/main/monitor/health_monitor.cpp @@ -29,6 +29,7 @@ #include "spi_master.h" #include "esp_event.h" #include "esp_netif_types.h" +#include "server/http_util.h" // ============================================================ // heap hook:捕获"破新低时的最后一次 alloc"(最后一击,未必是主因) @@ -399,8 +400,13 @@ void HealthMonitor::taskEntry(void *pvParameters) * 必须在 maybeRecord 之前,否则 est 长期偏低会误触发 hook 快照。 */ hookResyncEstFree(free_heap); + // 任务栈指标:本周期只采集一次,堆低水位快照和栈水位巡检共用。 + TaskStackMetric metrics[STACK_TASK_MAX]; + size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX); + // 堆低水位快照:仅在"最低空闲"创新低时记录一条上下文(5s 检测足够) - self.maybeRecordHeapLowWater(); + // 传入已采集的 metrics,避免 maybeRecordHeapLowWater 内部二次遍历任务表。 + self.maybeRecordHeapLowWater(metrics, count); if (free_heap < HEAP_WARNING_MIN) { /* 堆告警节流:OTA 期间阈值更低、间隔更长 */ @@ -426,10 +432,8 @@ void HealthMonitor::taskEntry(void *pvParameters) DEBUG_ERR("MON", "健康状态危险: %d", cur); } - // 栈水位主动巡检:复用 /system/status 的任务表,低水位时 DEBUG_ERR 主动告警。 + // 栈水位主动巡检:复用已采集的任务表,低水位时 DEBUG_ERR 主动告警。 // health_mon 任务不被 OTA 暂停,告警经 UDP/UART 直发,穿透 OTA 门控。 - TaskStackMetric metrics[STACK_TASK_MAX]; - size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX); for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) { if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) { continue; @@ -604,23 +608,6 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi) // ============================================================ // 堆低水位快照("最低空闲"创新低时记录上下文) // ============================================================ -namespace { -/** @brief snprintf 追加到 buf 的安全封装:截断但不越界,返回新 offset。 */ -int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...) -{ - if (offset < 0 || (size_t)offset >= cap) return offset; - va_list ap; - va_start(ap, fmt); - int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap); - va_end(ap); - if (n < 0) return offset; - if ((size_t)n >= cap - (size_t)offset) { - // 被截断:vsnprintf 返回的是"本应写入"的长度,按截断后修正 offset - return (int)cap - 1; - } - return offset + n; -} -} // namespace /** * @brief 检测"内部堆最低空闲"是否创新低,是则覆盖写入唯一快照槽。 @@ -630,7 +617,7 @@ int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...) * 所有外部读取(OtaCoordinator::getSnapshot / collectTaskStackMetrics / rssi / temp) * 都先在锁外取,避免持锁期间嵌套调用其它模块的锁;持锁后只做字段拷贝。 */ -void HealthMonitor::maybeRecordHeapLowWater() +void HealthMonitor::maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics, size_t pre_count) { // 与 http_handlers.cpp 同口径:内部 DRAM 数据堆 static constexpr uint32_t CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; @@ -691,8 +678,17 @@ void HealthMonitor::maybeRecordHeapLowWater() HeapLowTaskStack stacks[8]; uint8_t stack_count = 0; { - TaskStackMetric metrics[STACK_TASK_MAX]; - size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX); + // 优先复用巡检循环已采集的指标,避免二次遍历 + TaskStackMetric local_metrics[STACK_TASK_MAX]; + const TaskStackMetric *metrics; + size_t count; + if (pre_metrics && pre_count > 0) { + metrics = pre_metrics; + count = pre_count; + } else { + count = collectTaskStackMetrics(local_metrics, STACK_TASK_MAX); + metrics = local_metrics; + } // 先全部采集 struct Item { char name[16]; uint32_t rem; }; Item items[STACK_TASK_MAX]; @@ -816,7 +812,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const LockGuard lock(m_mutex); if (!m_heapLowWaterValid) { - return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}"); + return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}"); } const HeapLowWaterRecord &r = m_heapLowWater; @@ -824,7 +820,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const * hook_confidence 告知前端置信度:hook 命中=est_low_watermark,未命中=none。 * 前端应据此区分 estimated 与 measured(free_heap/min_free_heap 为实测)。 */ const char *hook_confidence = r.hook_fired ? "est_low_watermark" : "none"; - int pos = appendJsonfSafe(buf, cap, offset, + int pos = http_json_append(buf, cap, offset, "{\"count\":1,\"records\":[{" "\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu," "\"largest_free_block\":%lu,\"sys_state\":%u,\"ota_channel\":%u," @@ -863,24 +859,24 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const (unsigned long)r.hook_fired, (unsigned long)r.ip_event_count); if (pos == offset) { - return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}"); + return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}"); } for (uint8_t k = 0; k < r.task_stack_count && k < 8; ++k) { - int after = appendJsonfSafe(buf, cap, pos, + int after = http_json_append(buf, cap, pos, "%s{\"name\":\"%s\",\"remaining\":%lu}", k == 0 ? "" : ",", r.task_stacks[k].name, (unsigned long)r.task_stacks[k].remaining); if (after == pos) { - return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}"); + return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}"); } pos = after; } - int closed = appendJsonfSafe(buf, cap, pos, "]}]}"); + int closed = http_json_append(buf, cap, pos, "]}]}"); if (closed == pos) { - return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}"); + return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}"); } return closed; } diff --git a/main/monitor/health_monitor.h b/main/monitor/health_monitor.h index 543cbeb..2bf69a4 100644 --- a/main/monitor/health_monitor.h +++ b/main/monitor/health_monitor.h @@ -12,6 +12,7 @@ #include "driver/temperature_sensor.h" #include "esp_timer.h" #include "app_config.h" +#include "task_stack.h" // TaskStackMetric(maybeRecordHeapLowWater 可选参数) // T9.3: 健康状态等级 typedef enum { @@ -133,7 +134,11 @@ public: /** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。 * 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。 * 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。 */ - void maybeRecordHeapLowWater(); + /** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。 + * 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。 + * 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。 + * @param pre_metrics 巡检循环已采集的任务栈指标(避免二次遍历);null 时内部自采 */ + void maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics = nullptr, size_t pre_count = 0); /** @brief 将唯一快照序列化为 JSON:{"count":0|1,"records":[...]}。线程安全。 */ int dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const; diff --git a/main/monitor/motor_ota_relay.cpp b/main/monitor/motor_ota_relay.cpp index d49ae7d..b71f4d9 100644 --- a/main/monitor/motor_ota_relay.cpp +++ b/main/monitor/motor_ota_relay.cpp @@ -35,6 +35,59 @@ static int64_t nowMs() { return esp_timer_get_time() / 1000; } +/* ── 共享辅助:终态判断 + QUERY SPI 事务 ── */ + +/** @brief GD32 侧已进终态(IDLE/FINISHED/ERROR),喂数据应停止。 */ +static inline bool isTerminalState(motor_ota_state_t s) +{ + return s == MOTOR_OTA_IDLE || s == MOTOR_OTA_FINISHED || s == MOTOR_OTA_ERROR; +} + +/** + * @brief 发 SPI 0xD0 QUERY 查询 GD32 真实状态。 + * @param out_state 非 null 时写入 GD32 返回的 state + * @param out_err 非 null 时写入 error_code + * @param update_status true 时把结果同步写回 s_status(持锁) + * @return true=QUERY 成功且 ACK 有有效数据;false=SPI 失败或无 ACK + * + * 消除 feedTask 中 4 处重复的 qcmd[1]={2} + transact + parse qp[0..5] 块。 + */ +static bool queryMotorOtaStatus(motor_ota_state_t *out_state = nullptr, + uint8_t *out_err = nullptr, + bool update_status = false) +{ + uint8_t qcmd[1] = {2U}; /* cmd=QUERY */ + uint8_t qt = 0, qp[16] = {0}; + size_t ql = 0; + if (SpiMaster::instance().transact( + kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) != ESP_OK + || qt != kRspAck || ql < 1U) { + return false; + } + const motor_ota_state_t qst = static_cast(qp[0]); + if (out_state) *out_state = qst; + if (out_err) *out_err = (ql >= 2U) ? qp[1] : 0U; + if (update_status && s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + s_status.state = qst; + s_status.error_code = (ql >= 2U) ? qp[1] : 0U; + if (ql >= 6U) { + s_status.packets_acked = static_cast(qp[2]) + | (static_cast(qp[3]) << 8); + s_status.total_packets = static_cast(qp[4]) + | (static_cast(qp[5]) << 8); + } + if (ql >= 10U) { + s_status.fw_size = static_cast(qp[6]) + | (static_cast(qp[7]) << 8) + | (static_cast(qp[8]) << 16) + | (static_cast(qp[9]) << 24); + } + s_status.last_update_ms = nowMs(); + xSemaphoreGive(s_mutex); + } + return true; +} + /* ═══════════════════ 喂数据 task ═══════════════════ */ static void feedTask(void *arg) @@ -87,8 +140,7 @@ static void feedTask(void *arg) * (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。 */ const motor_ota_status_t st = motorOtaGetStatus(); if (nowMs() >= grace_end_ms) { - if (st.state == MOTOR_OTA_IDLE || st.state == MOTOR_OTA_FINISHED || - st.state == MOTOR_OTA_ERROR) { + if (isTerminalState(st.state)) { ESP_LOGW(TAG, "GD32 终态 0x%02X,停止喂数据 offset=%u/%u", (int)st.state, (unsigned)s_fw_offset, (unsigned)s_fw_size); break; @@ -127,36 +179,17 @@ const uint32_t remaining = s_fw_size - s_fw_offset; vTaskDelay(pdMS_TO_TICKS(10)); /* 快速重试(原 100ms 太慢导致吐量低) */ /* suppress 期间 s_status 不更新,连续失败时主动 QUERY GD32 真实状态, * 避免 GD32 已进 ERROR 终态但 feedTask 不知道、盲重试空转到 120s 超时。 */ - if (++consec_fail >= kQueryAfterFails) { - consec_fail = 0U; - uint8_t qcmd[1] = {2U}; /* cmd=QUERY */ - uint8_t qt = 0, qp[16] = {0}; - size_t ql = 0; - if (SpiMaster::instance().transact( - kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK - && qt == kRspAck && ql >= 1U) { - motor_ota_state_t qst = static_cast(qp[0]); - if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED || - qst == MOTOR_OTA_ERROR) { - ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u", - (int)qst, (unsigned)qp[1], - (unsigned)s_fw_offset, (unsigned)s_fw_size); - if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { - s_status.state = qst; - s_status.error_code = qp[1]; - if (ql >= 6U) { - s_status.packets_acked = static_cast(qp[2]) - | (static_cast(qp[3]) << 8); - s_status.total_packets = static_cast(qp[4]) - | (static_cast(qp[5]) << 8); - } - s_status.last_update_ms = nowMs(); - xSemaphoreGive(s_mutex); - } - break; - } - } - } + if (++consec_fail >= kQueryAfterFails) { + consec_fail = 0U; + motor_ota_state_t qst; + uint8_t qerr; + if (queryMotorOtaStatus(&qst, &qerr, true) && isTerminalState(qst)) { + ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u", + (int)qst, (unsigned)qerr, + (unsigned)s_fw_offset, (unsigned)s_fw_size); + break; + } + } continue; /* 不推进 offset,同 offset 重发 */ } consec_fail = 0U; @@ -177,13 +210,8 @@ const uint32_t remaining = s_fw_size - s_fw_offset; /* 握手未确认时定期 QUERY:确认 GD32 已过握手阶段才标 TRANSFER */ if (!handshake_confirmed && (ack_n & 7U) == 0U) { - uint8_t qcmd[1] = {2U}; - uint8_t qt = 0, qp[16] = {0}; - size_t ql = 0; - if (SpiMaster::instance().transact( - kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK - && qt == kRspAck && ql >= 1U) { - motor_ota_state_t qst = static_cast(qp[0]); + motor_ota_state_t qst; + if (queryMotorOtaStatus(&qst, nullptr, false)) { if (qst == MOTOR_OTA_ERASE || qst == MOTOR_OTA_TRANSFER || qst == MOTOR_OTA_FINISH_OTA || qst == MOTOR_OTA_WAIT_APP_RUN) { handshake_confirmed = true; @@ -216,16 +244,11 @@ const uint32_t remaining = s_fw_size - s_fw_offset; vTaskDelay(pdMS_TO_TICKS(2)); /* 连续 WAIT 超限:可能 GD32 auto-retry 重置了 s_fw_fed=0, * 当前 offset 被 gap 拒收(永远不会变 ACK)。QUERY 确认真实状态。 */ - if (++consec_wait >= kQueryAfterWaits) { - consec_wait = 0U; - uint8_t qcmd[1] = {2U}; /* cmd=QUERY */ - uint8_t qt = 0, qp[16] = {0}; - size_t ql = 0; - if (SpiMaster::instance().transact( - kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK - && qt == kRspAck && ql >= 2U) { - const motor_ota_state_t qst = static_cast(qp[0]); - const uint8_t qerr = qp[1]; + if (++consec_wait >= kQueryAfterWaits) { + consec_wait = 0U; + motor_ota_state_t qst; + uint8_t qerr; + if (queryMotorOtaStatus(&qst, &qerr, false)) { if (qst != MOTOR_OTA_TRANSFER) { /* GD32 不在传输态(auto-retry 回了 kPreCancel/握手): * 重置 offset 从头喂,配合 GD32 重新擦除+握手。 */ @@ -237,21 +260,20 @@ const uint32_t remaining = s_fw_size - s_fw_offset; } else { handshake_confirmed = true; /* GD32 在 transfer,确认握手完成 */ } - /* 终态仍 break */ - if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED || - qst == MOTOR_OTA_ERROR) { - ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止", - (unsigned)qst, (unsigned)qerr); - if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { - s_status.state = qst; - s_status.error_code = qerr; - s_status.last_update_ms = nowMs(); - xSemaphoreGive(s_mutex); - } - break; - } - } - } + /* 终态仍 break */ + if (isTerminalState(qst)) { + ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止", + (unsigned)qst, (unsigned)qerr); + if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) { + s_status.state = qst; + s_status.error_code = qerr; + s_status.last_update_ms = nowMs(); + xSemaphoreGive(s_mutex); + } + break; + } + } + } } else { /* 非 ACK/WAIT(含 ERROR/陈旧 type):不 break 整次 OTA。 * 曾因 poll 读到 EVENT/错 type 直接 break → fed 卡首 chunk。 */ @@ -273,53 +295,29 @@ const uint32_t remaining = s_fw_size - s_fw_offset; const uint32_t exit_deadline = nowMs() + kExitPollMs; bool got_terminal = false; while (nowMs() < exit_deadline) { - /* 优先吃 ring 推送(已恢复 suppress) */ - { - const motor_ota_status_t st = motorOtaGetStatus(); - if (st.state == MOTOR_OTA_FINISHED || st.state == MOTOR_OTA_ERROR || - st.state == MOTOR_OTA_IDLE) { - ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u", - (unsigned)st.state, (unsigned)st.error_code, - (unsigned)st.packets_acked, (unsigned)st.total_packets); - got_terminal = true; - break; - } - } - uint8_t qcmd[1] = {2U}; /* cmd=QUERY */ - uint8_t qresp_type = 0; - uint8_t qresp[16] = {0}; - size_t qresp_len = 0; - esp_err_t qr = SpiMaster::instance().transact( - kSpiMotorOtaCmd, qcmd, 1, - &qresp_type, qresp, sizeof(qresp), &qresp_len, 300U); - if (qr == ESP_OK && qresp_type == kRspAck && qresp_len >= 10U) { - const motor_ota_state_t qst = static_cast(qresp[0]); - if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { - s_status.state = qst; - s_status.error_code = qresp[1]; - s_status.packets_acked = static_cast(qresp[2]) - | (static_cast(qresp[3]) << 8); - s_status.total_packets = static_cast(qresp[4]) - | (static_cast(qresp[5]) << 8); - s_status.fw_size = static_cast(qresp[6]) - | (static_cast(qresp[7]) << 8) - | (static_cast(qresp[8]) << 16) - | (static_cast(qresp[9]) << 24); - s_status.last_update_ms = nowMs(); - xSemaphoreGive(s_mutex); - } - if (qst == MOTOR_OTA_FINISHED || qst == MOTOR_OTA_ERROR || - qst == MOTOR_OTA_IDLE) { - ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u", - (unsigned)qst, (unsigned)qresp[1], - (unsigned)(static_cast(qresp[2]) - | (static_cast(qresp[3]) << 8)), - (unsigned)(static_cast(qresp[4]) - | (static_cast(qresp[5]) << 8))); - got_terminal = true; - break; - } - } + /* 优先吃 ring 推送(已恢复 suppress) */ + { + const motor_ota_status_t st = motorOtaGetStatus(); + if (isTerminalState(st.state)) { + ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u", + (unsigned)st.state, (unsigned)st.error_code, + (unsigned)st.packets_acked, (unsigned)st.total_packets); + got_terminal = true; + break; + } + } + motor_ota_state_t qst; + uint8_t qerr; + if (queryMotorOtaStatus(&qst, &qerr, true)) { + if (isTerminalState(qst)) { + const motor_ota_status_t snap = motorOtaGetStatus(); + ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u", + (unsigned)qst, (unsigned)qerr, + (unsigned)snap.packets_acked, (unsigned)snap.total_packets); + got_terminal = true; + break; + } + } vTaskDelay(pdMS_TO_TICKS(kExitGapMs)); } if (!got_terminal) { diff --git a/main/monitor/motor_state.cpp b/main/monitor/motor_state.cpp index 91e928d..a33abc1 100644 --- a/main/monitor/motor_state.cpp +++ b/main/monitor/motor_state.cpp @@ -2,9 +2,7 @@ * motor_state.cpp - GD32 多电机状态推送解析与缓存 (SPI TYPE 0xC5) */ #include "monitor/motor_state.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" +#include "monitor/spi_ring_cache.h" #include @@ -13,21 +11,6 @@ namespace { static SemaphoreHandle_t s_mux = nullptr; static motor_state_table_t s_table = {}; -static uint32_t rdU32Le(const uint8_t *p) -{ - return static_cast(p[0]) - | (static_cast(p[1]) << 8) - | (static_cast(p[2]) << 16) - | (static_cast(p[3]) << 24); -} - -static void ensureMux() -{ - if (s_mux == nullptr) { - s_mux = xSemaphoreCreateMutex(); - } -} - } // namespace bool onMotorStateEvent(const uint8_t *payload, size_t len) @@ -51,7 +34,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len) return false; } - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) return false; if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false; @@ -102,7 +85,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len) bool getLatestMotorState(motor_state_table_t *out) { if (out == nullptr) return false; - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) return false; bool ok = false; @@ -118,7 +101,7 @@ bool getLatestMotorState(motor_state_table_t *out) uint32_t motorStateAgeMs(void) { - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) return UINT32_MAX; int64_t recv = 0; @@ -128,11 +111,5 @@ uint32_t motorStateAgeMs(void) recv = s_table.recv_ms; xSemaphoreGive(s_mux); } - if (!have || recv <= 0) return UINT32_MAX; - - const int64_t now = esp_timer_get_time() / 1000; - if (now < recv) return 0U; - const int64_t age = now - recv; - return (age > static_cast(UINT32_MAX)) ? UINT32_MAX - : static_cast(age); -} + return ringCacheAgeMs(have, recv); +} \ No newline at end of file diff --git a/main/monitor/motor_version.cpp b/main/monitor/motor_version.cpp index 527b54a..644c816 100644 --- a/main/monitor/motor_version.cpp +++ b/main/monitor/motor_version.cpp @@ -4,9 +4,7 @@ * 镜像 motor_state.cpp 的互斥保护缓存模型。 */ #include "monitor/motor_version.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/semphr.h" +#include "monitor/spi_ring_cache.h" #include @@ -15,21 +13,6 @@ namespace { static SemaphoreHandle_t s_mux = nullptr; static motor_version_table_t s_table = {}; -static uint32_t rdU32Le(const uint8_t *p) -{ - return static_cast(p[0]) - | (static_cast(p[1]) << 8) - | (static_cast(p[2]) << 16) - | (static_cast(p[3]) << 24); -} - -static void ensureMux() -{ - if (s_mux == nullptr) { - s_mux = xSemaphoreCreateMutex(); - } -} - } // namespace bool onMotorVersionEvent(const uint8_t *payload, size_t len) @@ -40,7 +23,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len) const uint8_t count = payload[0]; - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) return false; if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false; @@ -69,7 +52,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len) bool getLatestMotorVersion(motor_version_table_t *out) { if (out == nullptr) return false; - ensureMux(); + ringCacheEnsureMux(&s_mux); if (s_mux == nullptr) return false; bool ok = false; @@ -81,4 +64,4 @@ bool getLatestMotorVersion(motor_version_table_t *out) xSemaphoreGive(s_mux); } return ok; -} +} \ No newline at end of file diff --git a/main/monitor/spi_ring_cache.h b/main/monitor/spi_ring_cache.h new file mode 100644 index 0000000..a7f17d6 --- /dev/null +++ b/main/monitor/spi_ring_cache.h @@ -0,0 +1,62 @@ +/* + * spi_ring_cache.h - SPI ring push 缓存共享辅助 + * + * gd32_adc_feedback / motor_state / motor_version 三个模块共用相同的 + * 互斥懒创建 + 小端读 + ageMs 计算样板,提取到此处消除三份拷贝。 + */ +#ifndef SPI_RING_CACHE_H +#define SPI_RING_CACHE_H + +#include +#include +#include +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── 小端读 ── */ + +static inline uint16_t rdU16Le(const uint8_t *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static inline uint32_t rdU32Le(const uint8_t *p) +{ + return (uint32_t)p[0] + | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) + | ((uint32_t)p[3] << 24); +} + +/* ── 互斥懒创建 ── */ + +static inline void ringCacheEnsureMux(SemaphoreHandle_t *mux) +{ + if (*mux == nullptr) { + *mux = xSemaphoreCreateMutex(); + } +} + +/* ── 通用 ageMs ── + * 传入缓存的 have 标志 + recv_ms 时间戳,返回距 now 的毫秒数。 + * 无数据或 recv_ms<=0 返回 UINT32_MAX。 + */ +static inline uint32_t ringCacheAgeMs(bool have, int64_t recv_ms) +{ + if (!have || recv_ms <= 0) return UINT32_MAX; + const int64_t now = esp_timer_get_time() / 1000; + if (now < recv_ms) return 0U; + const int64_t age = now - recv_ms; + return (age > (int64_t)UINT32_MAX) ? UINT32_MAX : (uint32_t)age; +} + +#ifdef __cplusplus +} +#endif + +#endif // SPI_RING_CACHE_H \ No newline at end of file diff --git a/main/monitor/spi_speed_test.cpp b/main/monitor/spi_speed_test.cpp index 369a975..a04c28d 100644 --- a/main/monitor/spi_speed_test.cpp +++ b/main/monitor/spi_speed_test.cpp @@ -26,6 +26,7 @@ #include "esp_timer.h" #include "spi_master.h" #include "http_handlers.h" /* http_set_cors_headers */ +#include "server/http_util.h" #include "monitor/debug_log.h" namespace { @@ -37,30 +38,14 @@ constexpr size_t kPayloadDefault = 236U; /* 满帧:等效 0xD1 电机 OTA constexpr size_t kPayloadMax = 236U; /* APP kTxBufSize=256 - V2_OVERHEAD */ constexpr uint32_t kFrameTimeoutMs = 500U; -/** @brief 从 query 里取无符号整型参数,缺失/非法时返回默认值。 */ -uint32_t queryU32(httpd_req_t *req, const char *key, uint32_t defval) -{ - char qbuf[64]; - char vbuf[16]; - if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) != ESP_OK) { - return defval; - } - if (httpd_query_key_value(qbuf, key, vbuf, sizeof(vbuf)) != ESP_OK) { - return defval; - } - char *end = nullptr; - const unsigned long v = strtoul(vbuf, &end, 10); - return (end && *end == '\0') ? (uint32_t)v : defval; -} - } // namespace esp_err_t spi_speedtest_handler(httpd_req_t *req) { - uint32_t frames = queryU32(req, "frames", kFramesDefault); + uint32_t frames = http_query_u32(req, "frames", kFramesDefault); if (frames < 1U) { frames = 1U; } if (frames > kFramesMax) { frames = kFramesMax; } - size_t payload_len = (size_t)queryU32(req, "payload", (uint32_t)kPayloadDefault); + size_t payload_len = (size_t)http_query_u32(req, "payload", (uint32_t)kPayloadDefault); if (payload_len > kPayloadMax) { payload_len = kPayloadMax; } /* 探针 payload:递增填充(CRC32 覆盖,顺带校验数据路径完整性) */ @@ -74,9 +59,18 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req) uint64_t rtt_sum_us = 0U; uint64_t rtt_min_us = UINT64_MAX; uint64_t rtt_max_us = 0U; - /* RTT 分布直方图:<2 / 2-3 / 3-4 / 4-5 / >=5 ms,用于判断抖动形态 - * (双峰=离散 +1ms 碰撞;均匀=tick 相位量化) */ - uint32_t histo[5] = {0U, 0U, 0U, 0U, 0U}; + /* RTT 分布直方图(µs 计时,<2ms 区间细化为 0.5ms 粒度): + * <0.5 / 0.5-1 / 1-1.5 / 1.5-2 / 2-3 / 3-5 / >=5 ms, + * 用于判断抖动形态(<2ms 内的尖峰/双峰 vs 离散 +1ms 碰撞) */ + uint32_t histo[7] = {0U, 0U, 0U, 0U, 0U, 0U, 0U}; + /* 事务耗时(worker 内执行段,SpiMaster::lastWorkerExecUs)与 + * 调度/唤醒开销(RTT − 事务)拆解:事务慢=GD32 应答/SPI 时序; + * 调度慢=ESP32 任务抢占(WiFi/esp_timer 等)。 */ + uint64_t w_sum_us = 0U, w_min_us = UINT64_MAX, w_max_us = 0U; + uint64_t s_sum_us = 0U, s_min_us = UINT64_MAX, s_max_us = 0U; + /* 5 段细分(SpiMaster::SpiProfile):提交/调度、发送帧、等应答、读响应、唤醒 */ + uint64_t seg_sum[5] = {0U, 0U, 0U, 0U, 0U}; + uint64_t seg_max[5] = {0U, 0U, 0U, 0U, 0U}; DEBUG_INFO("SPI", "[SPEEDTEST] start frames=%u payload=%u", (unsigned)frames, (unsigned)payload_len); @@ -100,12 +94,34 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req) rtt_sum_us += rtt; if (rtt < rtt_min_us) { rtt_min_us = rtt; } if (rtt > rtt_max_us) { rtt_max_us = rtt; } - const uint32_t ms_bucket = (uint32_t)(rtt / 1000U); - if (ms_bucket < 2U) { ++histo[0]; } - else if (ms_bucket < 3U) { ++histo[1]; } - else if (ms_bucket < 4U) { ++histo[2]; } - else if (ms_bucket < 5U) { ++histo[3]; } - else { ++histo[4]; } + const uint64_t wus = SpiMaster::instance().lastWorkerExecUs(); + const uint64_t sus = (rtt > wus) ? (rtt - wus) : 0U; + w_sum_us += wus; + if (wus < w_min_us) { w_min_us = wus; } + if (wus > w_max_us) { w_max_us = wus; } + s_sum_us += sus; + if (sus < s_min_us) { s_min_us = sus; } + if (sus > s_max_us) { s_max_us = sus; } + /* 5 段细分(防回绕钳 0) */ + const SpiMaster::SpiProfile &pf = SpiMaster::instance().lastProfile(); + const uint64_t seg[5] = { + (pf.t_w0 > pf.t_submit0) ? (pf.t_w0 - pf.t_submit0) : 0U, + (pf.t_send1 > pf.t_send0) ? (pf.t_send1 - pf.t_send0) : 0U, + (pf.t_wait1 > pf.t_wait0) ? (pf.t_wait1 - pf.t_wait0) : 0U, + (pf.t_read1 > pf.t_wait1) ? (pf.t_read1 - pf.t_wait1) : 0U, + (pf.t_submit1 > pf.t_w1) ? (pf.t_submit1 - pf.t_w1) : 0U, + }; + for (int k = 0; k < 5; ++k) { + seg_sum[k] += seg[k]; + if (seg[k] > seg_max[k]) { seg_max[k] = seg[k]; } + } + if (rtt < 500U) { ++histo[0]; } + else if (rtt < 1000U) { ++histo[1]; } + else if (rtt < 1500U) { ++histo[2]; } + else if (rtt < 2000U) { ++histo[3]; } + else if (rtt < 3000U) { ++histo[4]; } + else if (rtt < 5000U) { ++histo[5]; } + else { ++histo[6]; } } else { ++n_fail; } @@ -120,25 +136,44 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req) tput_kbps = ((double)n_ok * (double)payload_len) / ((double)elapsed_us / 1000000.0) / 1024.0; } if (rtt_min_us == UINT64_MAX) { rtt_min_us = 0U; } + if (w_min_us == UINT64_MAX) { w_min_us = 0U; } + if (s_min_us == UINT64_MAX) { s_min_us = 0U; } DEBUG_INFO("SPI", "[SPEEDTEST] done ok=%u fail=%u avg=%uus tput=%.1fKB/s", (unsigned)n_ok, (unsigned)n_fail, (unsigned)rtt_avg_us, tput_kbps); - char text[480]; + char text[640]; const int len = snprintf(text, sizeof(text), "SPI 测速结果\n" "帧数: %u 成功: %u 失败: %u%s\n" "载荷: %u B/帧 总耗时: %u ms\n" - "往返延迟: 平均 %.1f ms 最快 %.1f ms 最慢 %.1f ms\n" - "延迟分布: <2ms:%u 2-3ms:%u 3-4ms:%u 4-5ms:%u >=5ms:%u\n" + "往返延迟: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n" + "SPI 事务: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n" + "调度开销: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n" + "单帧分段 平均: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n" + "单帧分段 最慢: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n" + "延迟分布: <0.5ms:%u 0.5-1ms:%u 1-1.5ms:%u 1.5-2ms:%u 2-3ms:%u 3-5ms:%u >=5ms:%u\n" "等效吞吐: %.1f KB/s\n", (unsigned)frames, (unsigned)n_ok, (unsigned)n_fail, (n_fail > 0U) ? " <-- 存在失败帧, 链路异常!" : "", (unsigned)payload_len, (unsigned)elapsed_ms, - (double)rtt_avg_us / 1000.0, (double)rtt_min_us / 1000.0, - (double)rtt_max_us / 1000.0, + (unsigned long long)rtt_avg_us, (unsigned long long)rtt_min_us, + (unsigned long long)rtt_max_us, + (unsigned long long)(w_sum_us / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)w_min_us, (unsigned long long)w_max_us, + (unsigned long long)(s_sum_us / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)s_min_us, (unsigned long long)s_max_us, + (unsigned long long)(seg_sum[0] / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)(seg_sum[1] / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)(seg_sum[2] / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)(seg_sum[3] / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)(seg_sum[4] / (n_ok > 0U ? n_ok : 1U)), + (unsigned long long)seg_max[0], (unsigned long long)seg_max[1], + (unsigned long long)seg_max[2], (unsigned long long)seg_max[3], + (unsigned long long)seg_max[4], (unsigned)histo[0], (unsigned)histo[1], (unsigned)histo[2], - (unsigned)histo[3], (unsigned)histo[4], tput_kbps); + (unsigned)histo[3], (unsigned)histo[4], + (unsigned)histo[5], (unsigned)histo[6], tput_kbps); http_set_cors_headers(req); httpd_resp_set_type(req, "text/plain; charset=utf-8"); diff --git a/main/ota/esp32_ota_core.cpp b/main/ota/esp32_ota_core.cpp index d7b905f..93bfa50 100644 --- a/main/ota/esp32_ota_core.cpp +++ b/main/ota/esp32_ota_core.cpp @@ -19,6 +19,7 @@ #include "esp32_ota_core.h" #include "app_config.h" #include "monitor/debug_log.h" +#include "spi_master.h" /* setSpiPaused:OTA 期间暂停 SPI,避免 flash 写与 SPI 读并发 */ #define TAG "OTA_CORE" @@ -110,6 +111,7 @@ esp_err_t Esp32OtaCore::finalizeCleanupAndReturn(esp_err_t code) } m_ended = false; releaseMutex(); + SpiMaster::instance().setSpiPaused(false); /* OTA 结束(失败也恢复 SPI) */ resetState(); return code; } @@ -201,6 +203,12 @@ esp_err_t Esp32OtaCore::begin(size_t total_size) m_ended = false; memset(m_sha256, 0, sizeof(m_sha256)); + /* ESP32 自我 OTA 期间暂停 SPI(GD32 按铃读 + 业务 transact 全部停摆): + * esp_ota_write 写 flash 时 cache 反复失效,SPI worker 的 flash 驻留代码 + * 访问会被拖慢甚至触发 Cache error,且此时 GD32 数据无意义。 + * finalize()/abort() 恢复。 */ + SpiMaster::instance().setSpiPaused(true); + DEBUG_DBG(TAG, "ESP32 OTA begin part=%s size=%u (erase-on-write)", m_partition->label, (unsigned)total_size); return ESP_OK; @@ -382,11 +390,13 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot, if (err != ESP_OK) { DEBUG_ERR(TAG, "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err)); releaseMutex(); + SpiMaster::instance().setSpiPaused(false); /* OTA 失败也恢复 SPI */ resetState(); return err; } releaseMutex(); + SpiMaster::instance().setSpiPaused(false); /* OTA 收尾完成,恢复 SPI 业务 */ DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,共 %u 字节", (unsigned)m_received); // 保持 m_active 直到重启或下次 begin;resetState 由 begin/abort 处理 @@ -413,6 +423,7 @@ void Esp32OtaCore::abort() esp_ota_abort(m_handle); } releaseMutex(); + SpiMaster::instance().setSpiPaused(false); /* OTA 中止,恢复 SPI 业务 */ resetState(); DEBUG_INFO(TAG, "ESP32 OTA 已中止并清理"); } diff --git a/main/ota/ota_coordinator.cpp b/main/ota/ota_coordinator.cpp index a120652..0318ab5 100644 --- a/main/ota/ota_coordinator.cpp +++ b/main/ota/ota_coordinator.cpp @@ -100,6 +100,24 @@ void OtaCoordinator::watchdogCheckLocked() doAbortLocked(); } +esp_err_t OtaCoordinator::beginLockedWithChecks(const char *tag, bool &out_lock) +{ + out_lock = lock(); + if (!out_lock) return ESP_ERR_INVALID_STATE; + + // ★ 卡死自愈检查 + watchdogCheckLocked(); + + // ★ 电机 OTA 互斥:电机喂数期间禁止新的 GD32/ESP32 OTA + if (motorOtaIsActive()) { + DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 %s", tag); + unlock(true); + out_lock = false; + return ESP_ERR_INVALID_STATE; + } + return ESP_OK; +} + // ============================================================ // 初始化 // ============================================================ @@ -138,24 +156,15 @@ void OtaCoordinator::init() // ============================================================ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel) { - bool h = lock(); - - // ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程 - if (h) watchdogCheckLocked(); - - // ★ 电机 OTA 互斥:电机喂数任务与 GD32 固件共用 gd32_ota 分区, - // 电机喂数期间禁止新的 GD32 存储,避免分区被并发覆写。 - if (h && motorOtaIsActive()) { - DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 GD32 存储"); - unlock(h); - return ESP_ERR_INVALID_STATE; - } + bool h; + esp_err_t pre = beginLockedWithChecks("GD32 存储", h); + if (pre != ESP_OK) return pre; // ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道, // 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。 // 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。 // RELAYING 是中继阶段,不可中断,也拒绝。 - if (h && m_sysState == OTA_SYS_GD32_STORING) { + if (m_sysState == OTA_SYS_GD32_STORING) { if (m_otaChannel != channel) { DEBUG_WARN(TAG, "GD32存中 ch=%d 拒跨通道 ch=%d", (int)m_otaChannel, (int)channel); @@ -169,33 +178,31 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch // 系统忙检查:仅拒绝真正"在传输"的活跃状态 // STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖 - if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING || + if (m_sysState == OTA_SYS_ESP32_RECEIVING || m_sysState == OTA_SYS_ESP32_FINALIZING || m_sysState == OTA_SYS_ESP32_REBOOTING || - m_sysState == OTA_SYS_GD32_RELAYING)) { + m_sysState == OTA_SYS_GD32_RELAYING) { DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState); unlock(h); return ESP_ERR_INVALID_STATE; } // 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。 - // 注意:清理 NVS meta 不在此处做,避免与紧跟的 prepareGd32Storage 内 - // clearGd32Meta + saveGd32Meta 重复一次写入,仅做状态注释说明。 - if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) { + if (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE) { DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size); } - if (h) { - m_otaEpoch++; // ★ 新会话开始,递增 epoch - m_otaChannel = channel; // ★ 记录发起通道 - m_sysState = OTA_SYS_GD32_STORING; - m_gd32StorageDone = false; - m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻 - DEBUG_DBG(TAG, "GD32存开始 %" PRIu32 "B →STORING ep=%u ch=%d", - total_size, m_otaEpoch, (int)channel); - } + m_otaEpoch++; // ★ 新会话开始,递增 epoch + m_otaChannel = channel; // ★ 记录发起通道 + m_sysState = OTA_SYS_GD32_STORING; + m_gd32StorageDone = false; + m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻 + DEBUG_DBG(TAG, "GD32存开始 %" PRIu32 "B →STORING ep=%u ch=%d", + total_size, m_otaEpoch, (int)channel); + // ★ 预分配 flashWriteChunk 对齐缓冲(OTA 期消除逐块 malloc;非 OTA 期零占用) + Gd32FlashStore::acquireAlignBufForOta(); unlock(h); - return h ? ESP_OK : ESP_ERR_INVALID_STATE; + return ESP_OK; } esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t channel) @@ -416,23 +423,14 @@ void OtaCoordinator::notifyBleCoexEnd() // ============================================================ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel) { - bool h = lock(); - - // ★ 卡死自愈检查 - if (h) watchdogCheckLocked(); - - // ★ 电机 OTA 互斥:ESP32 自身 OTA 会重启设备,中断正在进行的电机喂数, - // 电机 OTA 期间拒绝 ESP32 OTA。 - if (h && motorOtaIsActive()) { - DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 ESP32 OTA"); - unlock(h); - return ESP_ERR_INVALID_STATE; - } + bool h; + esp_err_t pre = beginLockedWithChecks("ESP32 OTA", h); + if (pre != ESP_OK) return pre; // ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道, // 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。 // 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。 - if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) { + if (m_sysState == OTA_SYS_ESP32_RECEIVING) { if (m_otaChannel != channel) { DEBUG_WARN(TAG, "ESP32收中 ch=%d 拒跨通道 ch=%d", (int)m_otaChannel, (int)channel); @@ -447,7 +445,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann // ★ FINALIZING 残留自愈:SHA 不匹配时 core 已 abort,但旧固件可能未把协调器 // 复位到 IDLE。core 已空闲 ⇒ 协调器状态是假占用,允许清掉后开新会话。 // core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。 - if (h && m_sysState == OTA_SYS_ESP32_FINALIZING) { + if (m_sysState == OTA_SYS_ESP32_FINALIZING) { if (!Esp32OtaCore::instance().isActive()) { DEBUG_WARN(TAG, "FINALIZING残留(core空闲),强制复位"); doAbortLocked(); @@ -459,28 +457,25 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann } // 系统忙检查:GD32 传输/中继进行中,或 ESP32 重启中,均拒绝。 - if (h && (m_sysState == OTA_SYS_GD32_STORING || - m_sysState == OTA_SYS_GD32_RELAYING || - m_sysState == OTA_SYS_ESP32_REBOOTING)) { + if (m_sysState == OTA_SYS_GD32_STORING || + m_sysState == OTA_SYS_GD32_RELAYING || + m_sysState == OTA_SYS_ESP32_REBOOTING) { DEBUG_WARN(TAG, "忙 state=%d(%s) 拒ESP32 OTA", (int)m_sysState, stateToString(m_sysState)); unlock(h); return ESP_ERR_INVALID_STATE; } - esp_err_t err = ESP_ERR_INVALID_STATE; - if (h) { - // 委托 Esp32OtaCore 处理分区/互斥逻辑 - err = Esp32OtaCore::instance().begin(total_size); - if (err == ESP_OK) { - m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效) - m_otaChannel = channel; // ★ 记录发起通道 - m_sysState = OTA_SYS_ESP32_RECEIVING; - m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻 - pauseCanFeedbackTask(true); - } else { - DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err)); - } + // 委托 Esp32OtaCore 处理分区/互斥逻辑 + esp_err_t err = Esp32OtaCore::instance().begin(total_size); + if (err == ESP_OK) { + m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效) + m_otaChannel = channel; // ★ 记录发起通道 + m_sysState = OTA_SYS_ESP32_RECEIVING; + m_busySinceTick = xTaskGetTickCount(); // ★ 记录起始时刻 + pauseCanFeedbackTask(true); + } else { + DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err)); } unlock(h); @@ -622,9 +617,11 @@ void OtaCoordinator::doAbortLocked() if (m_sysState == OTA_SYS_GD32_STORING) { Gd32FlashStore::instance().metaClear(); + Gd32FlashStore::releaseAlignBufForOta(); // 释放 OTA 期对齐缓冲 } else if (m_sysState == OTA_SYS_GD32_RELAYING) { /* 中继中止必须 CANCEL 远端,否则 GD32 停在 WAITING_* 半会话 */ OtaRelay::instance().abortRemoteAndReset(true); + Gd32FlashStore::releaseAlignBufForOta(); // 中继阶段也可能写过 flash } else if (m_sysState == OTA_SYS_ESP32_RECEIVING || m_sysState == OTA_SYS_ESP32_FINALIZING) { Esp32OtaCore::instance().abort(); @@ -761,12 +758,6 @@ const char* OtaCoordinator::stateToString(ota_system_state_t s) } } -/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */ -bool OtaCoordinator::getGd32AppAlive() const -{ - return OtaRelay::getGd32AppAlive(); -} - // ============================================================ // OTA 状态快照 // ============================================================ diff --git a/main/ota/ota_coordinator.h b/main/ota/ota_coordinator.h index 38b0892..873476d 100644 --- a/main/ota/ota_coordinator.h +++ b/main/ota/ota_coordinator.h @@ -420,12 +420,6 @@ public: bool isIdle() const; // ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)── - /** - * @brief 获取 GD32 APP 存活状态。 - * @return true 表示 GD32 APP 存活。 - */ - bool getGd32AppAlive() const; - // ── 完全复位 ── /** * @brief 完全复位协调器和所有子系统状态。 @@ -479,6 +473,13 @@ private: // 已持锁中止:清理子系统并将状态复位为 IDLE void doAbortLocked(); + /** @brief 统一 "lock + watchdog 自愈 + 电机 OTA 互斥拒绝" 前缀检查。 + * beginGd32Storage / beginEsp32Ota 共用此逻辑。 + * @param tag 日志标签(如 "GD32 存储" / "ESP32 OTA") + * @param out_lock 成功时输出锁句柄(调用方负责 unlock) + * @return ESP_OK=前缀通过可继续;ESP_ERR_INVALID_STATE=被拒绝(已 unlock) */ + esp_err_t beginLockedWithChecks(const char *tag, bool &out_lock); + // ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。 // 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。 void setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg); diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index 9a8e36a..05e012e 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -349,10 +349,14 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets); - /* 转发预算:自本阶段起算。正常 ~15ms/包,预算 40ms/包 + 20s 底(上限 120s)。 */ - constexpr uint32_t kXferMinMs = 20U * 1000U; + /* 转发预算:自本阶段起算。正常 ~2ms/包(12MHz+ 读预算收紧后实测), + * 单包失败重发最坏 ~95ms/轮 × 3 轮 ≈ 285ms(底层 3 次尝试 + relay 50ms 窗口)。 + * 预算按"正常 + 重发余量":25ms/包 ≈ 正常 2ms + 约 10% 包重发 1 次的平均, + * 底 10s 覆盖慢启动/握手余量(777 包正常仅 ~1.6s);上限 120s 兜极端慢速。 + * 跨包卡死(10s 无成功帧)由 kStallTimeoutMs 单独检测,总预算不重复兜底。 */ + constexpr uint32_t kXferMinMs = 10U * 1000U; constexpr uint32_t kXferMaxMs = 120U * 1000U; - constexpr uint32_t kXferPerPktMs = 40U; + constexpr uint32_t kXferPerPktMs = 25U; uint32_t xfer_ms = kXferMinMs + m_transferTotalPackets * kXferPerPktMs; if (xfer_ms > kXferMaxMs) { xfer_ms = kXferMaxMs; @@ -448,9 +452,6 @@ ota_result_t OtaRelay::forwardFirmware() DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size); - /* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */ - SpiMaster::instance().setSuppressRingService(true); - /* 完整性门控(避免与 /ota/relay·BLE 触发前的 verifyGd32Firmware 二次全盘 SHA): * - meta 必须完整 stored≥total * - checksum 非全零:触发路径已 finalize+比对,此处信任 meta,跳过再扫 Flash @@ -500,10 +501,8 @@ ota_result_t OtaRelay::forwardFirmware() DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState()); /* 必须通知 GD32 CANCEL,否则远端停在 WAITING_* 半会话 */ abortRemoteAndReset(true); - } else { - /* 成功路径:解除按铃抑制(reset 也会做,成功时状态机可能停在 DONE) */ - SpiMaster::instance().setSuppressRingService(false); } + /* 成功/失败统一由 reset() 解除 ring 抑制(relay task 侧持有 ref) */ return res; } @@ -1174,7 +1173,10 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], * - 等 ACK 期间可预组下一包(与从机处理重叠)。 */ constexpr uint32_t kMaxDataSends = 3U; - constexpr uint32_t kAckWaitMs = 6000U; /* 单次发送后等 ACK 上限(覆盖 GD32 增量 drain) */ + /* 单次发送后等 ACK 上限:GD32 正常 ms 级回 ACK(ISR 回 WAIT、主循环回 ACK), + * 50ms 未到即重发(DUP-SEQ 回放捞回),不再干等 6s(用户指定 50ms)。 + * credit==0 的排空积压由 transact 内部 credit 背压消化,此处无需长等。 */ + constexpr uint32_t kAckWaitMs = 50U; constexpr uint32_t kPollSliceMs = 50U; /* 略紧:DATA_READY 驱动,缩短空切片 */ TickType_t pkt_start = xTaskGetTickCount(); @@ -1204,9 +1206,11 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64], { resp_len = 0; resp_type = 0; + /* 等首响应上限 10ms:一次 SPI 事务最多 5ms(用户指定), + * 超时立即进入下方 50ms poll 窗口,不再干等 200ms。 */ esp_err_t err = SpiMaster::instance().transact( SPI_OTA_CMD_DATA, tx_buf, 64, - &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200); + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 10); logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len); if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) { saw_wait = true; @@ -1412,6 +1416,58 @@ bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime) return false; } +/** + * @brief 统一 "HELLO(role=APP) → 可选 PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。 + * + * 两处原循环差异仅:taskEntry 在 HELLO 已带身份(gd32IdKnown)时跳过 PING 直接成功。 + * 用 allow_hello_shortcut 参数统一:true 时启用 HELLO 身份快路径。 + * + * @param timeout_ms PING 超时 + * @param ping_interval_ms 每轮间隔 + * @param max_attempts 最大轮数 + * @param allow_hello_shortcut true=HELLO 带身份即成功(taskEntry 场景) + * @return true=APP 存活已确认 + */ +bool OtaRelay::helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms, + uint32_t max_attempts, bool allow_hello_shortcut) +{ + bool session_ok = false; + for (uint32_t attempt = 0; attempt < max_attempts; ++attempt) { + vTaskDelay(pdMS_TO_TICKS(ping_interval_ms)); + + if (!session_ok) { + if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK && + SpiMaster::instance().gd32Role() == V2_ROLE_APP) { + session_ok = true; + /* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */ + if (allow_hello_shortcut && SpiMaster::instance().gd32IdKnown()) { + StateLockGuard lock; + gd32Version = SpiMaster::instance().gd32VersionCached(); + gd32AppAlive = true; + DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d", + gd32Version.app_major, gd32Version.app_minor, + gd32Version.app_patch, gd32Version.app_build); + DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d", + gd32Version.boot_major, gd32Version.boot_minor, + gd32Version.boot_patch, gd32Version.boot_build); + return true; + } + } else { + DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts); + continue; + } + } + /* HELLO 成功但无身份(旧固件),或 doWaitForAppAlive 路径:回退 APP PING */ + if (probeAndCacheAppAlive(timeout_ms)) { + return true; + } + /* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */ + session_ok = false; + DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts); + } + return false; +} + /** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */ ota_result_t OtaRelay::doSendFinishOta() { @@ -1485,30 +1541,8 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms) /* FINISH 后短暂安静,避免 jump/APP SPI arm 窗口被 HELLO 打乱(对齐既有 300ms 节拍)。 */ vTaskDelay(pdMS_TO_TICKS(500)); - /* APP 起机约数秒;HELLO 须 role=APP 再建 session,再 appPing。 */ - bool session_established = false; - for (uint32_t attempt = 0; attempt < 20; ++attempt) { - vTaskDelay(pdMS_TO_TICKS(300)); - - if (!session_established) { - esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP); - if (hr == ESP_OK && SpiMaster::instance().gd32Role() == V2_ROLE_APP) { - session_established = true; - } else { - DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1); - continue; - } - } - - if (probeAndCacheAppAlive(timeout_ms)) { - break; - } - /* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */ - session_established = false; - DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1); - } - - if (!getGd32AppAlive()) { + /* doWaitForAppAlive 不启用 HELLO 身份快路径:OTA 完成后必须 PING 确认 APP 真正运行 */ + if (!helloAndProbeApp(timeout_ms, 300U, 20U, /*allow_hello_shortcut=*/false)) { DEBUG_ERR("OTA", "GD32 APP 启动确认超时,OTA 失败"); /* APP 未起来时 CANCEL 可能无效,仍尽力清理 BOOT 半会话 */ abortRemoteAndReset(true); @@ -1562,42 +1596,7 @@ void OtaRelay::taskEntry(void *pvParameters) /* 启动 GD32 探测:先 hello 建 v2 session,再 appPing。 */ { DEBUG_INFO("OTA", "启动初始 GD32 探测 (hello+probe)..."); - bool session_ok = false; - bool alive = false; - for (uint32_t attempt = 0; attempt < 20U; ++attempt) { - vTaskDelay(pdMS_TO_TICKS(300)); - if (!session_ok) { - if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK && - SpiMaster::instance().gd32Role() == V2_ROLE_APP) { - session_ok = true; - /* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */ - if (SpiMaster::instance().gd32IdKnown()) { - StateLockGuard lock; - gd32Version = SpiMaster::instance().gd32VersionCached(); - gd32AppAlive = true; - alive = true; - DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d", - gd32Version.app_major, gd32Version.app_minor, - gd32Version.app_patch, gd32Version.app_build); - DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d", - gd32Version.boot_major, gd32Version.boot_minor, - gd32Version.boot_patch, gd32Version.boot_build); - break; - } - } else { - DEBUG_INFO("OTA", "等待 GD32 APP... (%" PRIu32 "/20)", attempt + 1U); - continue; - } - } - /* 旧固件无 HELLO 身份:回退 APP PING */ - if (probeAndCacheAppAlive(2000)) { - alive = true; - break; - } - session_ok = false; - DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/20)", attempt + 1U); - } - if (!alive) { + if (!helloAndProbeApp(2000U, 300U, 20U, /*allow_hello_shortcut=*/true)) { DEBUG_WARN("OTA", "启动 GD32 探测未成功(中继前仍可再 hello)"); } } @@ -1610,6 +1609,11 @@ void OtaRelay::taskEntry(void *pvParameters) } DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg); + /* 尽早抑制 ring service:onRelayStarted 可能因协调器锁(/system/status + * 等争锁)阻塞,此窗口内 worker 会把上一阶段残留 RESPONSE 当 EVENT 偷读 + * (线上 log#1:TRANSFERRING 起始瞬间 serviceRing 读到 type=0x80)。 + * forwardFirmware 不再重复设置;释放统一走 reset()。 */ + SpiMaster::instance().setSuppressRingService(true); // 通过 IRelayListener 接口回调通知协调器 IRelayListener *listener = getRelayListener(); if (listener) listener->onRelayStarted(); diff --git a/main/relay/ota_relay.h b/main/relay/ota_relay.h index 6f6d330..38f6d84 100644 --- a/main/relay/ota_relay.h +++ b/main/relay/ota_relay.h @@ -118,16 +118,6 @@ public: */ ota_result_t forwardFirmware(); - // ── 事件驱动接口 ── - /** - * @brief 向状态机投递事件,驱动状态转换. - * @param event 事件类型. - * @param payload 关联数据(响应载荷等),可为nullptr. - * @param len 数据长度. - * @return 当前操作结果码(OTA_RESULT_OK 表示事件已处理且可继续). - */ - ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len); - // ── 状态查询(线程安全)── /** * @brief 获取当前中继状态. @@ -145,31 +135,6 @@ public: * @return 中继进度快照。 */ static RelaySnapshot getSnapshot(); - /** - * @brief 设置中继状态. - * @param s 目标状态。 - */ - static void setState(relay_state_t s); - /** - * @brief 设置中继当前序号. - * @param seq 序号值。 - */ - static void setRelaySeq(uint32_t seq); - /** - * @brief 设置中继总包数. - * @param total 总包数。 - */ - static void setRelayTotalPackets(uint32_t total); - /** - * @brief 设置GD32 APP存活标志位. - * @param alive true 表示存活。 - */ - static void setGd32AppAlive(bool alive); - /** - * @brief 设置GD32固件版本. - * @param ver 版本信息结构体引用。 - */ - static void setGd32Version(const gd32_version_t &ver); /** * @brief 获取 OTA 中继任务句柄。 * @return FreeRTOS 任务句柄。 @@ -195,11 +160,6 @@ public: * @param listener 监听器指针,传 nullptr 取消注册。 */ static void setRelayListener(IRelayListener *listener); - /** - * @brief 获取当前注册的监听器。 - * @return 监听器指针,未注册时为 nullptr。 - */ - static IRelayListener* getRelayListener(); // ── 队列访问器 ── /** @@ -223,6 +183,19 @@ public: */ static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr); + /** @brief 统一 "HELLO(role=APP) → PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。 */ + static bool helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms, + uint32_t max_attempts, bool allow_hello_shortcut); + + // ── 内部状态写入(仅 ota_relay.cpp 使用,无外部调用)── + static void setState(relay_state_t s); + static void setRelaySeq(uint32_t seq); + static void setRelayTotalPackets(uint32_t total); + static void setGd32AppAlive(bool alive); + static void setGd32Version(const gd32_version_t &ver); + static IRelayListener* getRelayListener(); + ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len); + // ── 线程安全锁 ── static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用) static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间 diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index be05286..a0b2f3e 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -11,7 +11,11 @@ void drainOtaDataQueue() { BleOtaDataBatch stale; while (s_otaDataQueue && xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) { - if (stale.data) free(stale.data); + if (stale.pool_idx >= 0) { + ble_ota_pool_release(stale.pool_idx); // 静态池槽:归还 + } else if (stale.data) { + free(stale.data); // heap 回退路径:释放 + } } } diff --git a/main/server/ble_ota_data.cpp b/main/server/ble_ota_data.cpp index f644570..5c7042e 100644 --- a/main/server/ble_ota_data.cpp +++ b/main/server/ble_ota_data.cpp @@ -54,6 +54,132 @@ ota_meta_t s_ble_gd32_meta = {}; bool s_ble_gd32_meta_active = false; bool s_conn_fallback_requested = false; +// ============================================================ +// DATA_IN 缓冲池(2026-08 内存优化) +// +// 背景:原 DATA_IN 路径每包 malloc(recv_len) + worker free(item.data), +// BLE 高速下发时高频堆抖动制造碎片,挤掉 lwip pbuf 连续块导致网络断连。 +// +// 策略:OTA begin 时一次性 calloc 池(连续大块,比高频 per-pack malloc +// 碎片小得多),DATA_IN 借槽拷贝 → 入队 → worker 处理后归还; +// OTA end/abort/断连时 free 整个池。 +// - 非 OTA 期间:池指针为 NULL,零占用,堆基线不受影响。 +// - OTA 期间:借/还 O(1) 标志位操作,无堆抖动。 +// - 池满/包过大回退 malloc + 负 ACK(保留原语义,避免功能回退)。 +// +// 池容量 8 槽 × 512B = 4KB(2026-08-01 修正): +// 原 8×4096B=32KB 使 OTA 全程多占 28KB 堆,刷写 1MB 固件收尾时 +// (0xF8 后需创建 finish task + image verify)直接 ESP_ERR_NO_MEM。 +// 单次 ATT 写入上限 = MTU 512B(BLE_MTU_SIZE),常态包 ≤512B 全走池; +// NimBLE 合并的大 batch(≤4096B,经 BLE_OTA_BUF_SIZE 截断)回退 +// malloc,与无池时代行为一致。4KB 固定占用对收尾任务足够安全。 +// ============================================================ +struct BleDataPool { + static constexpr size_t kSlots = 8; + static constexpr size_t kSlotSize = 512; // 单次 ATT 写入上限(MTU);大 batch 回退 malloc + uint8_t *buf[kSlots]; // kSlots 个独立槽(calloc 一次分配 kSlots*kSlotSize 后切分) + volatile bool in_use[kSlots]; +}; +static BleDataPool *s_data_pool = nullptr; + +// 前向声明(poolAlloc 与 poolFree 互相引用) +static void poolFree(); + +/** @brief OTA begin 时分配池。失败返回 false(调用方回退纯 malloc 路径)。 + * 安全性:上次会话遗留的池仅当全部槽空闲才复用。若 worker 仍持有在途 + * batch(in_use=true),强制复位复用会让新会话 DATA_IN 覆写旧 worker + * 正在解析的内存 → flash 写坏 → 收尾 SHA/verify 失败。此时本会话回退 + * malloc,旧池留待 worker 归还后由下次 poolFree 释放。 */ +static bool poolAlloc() +{ + if (s_data_pool) { + // 上次会话遗留池:检查是否全部空闲(in_use 仅由 worker/队列消费侧复位) + for (size_t i = 0; i < BleDataPool::kSlots; ++i) { + if (s_data_pool->in_use[i]) { + DEBUG_WARN(TAG, "DATA_IN 池槽仍被占用,本会话回退 malloc"); + return false; + } + } + return true; // 全部空闲:安全复用 + } + s_data_pool = (BleDataPool *)calloc(1, sizeof(BleDataPool)); + if (!s_data_pool) return false; + // 一次性分配所有槽的负载数据(连续大块,碎片友好) + uint8_t *blob = (uint8_t *)calloc(BleDataPool::kSlots, BleDataPool::kSlotSize); + if (!blob) { + free(s_data_pool); + s_data_pool = nullptr; + return false; + } + for (size_t i = 0; i < BleDataPool::kSlots; ++i) { + s_data_pool->buf[i] = blob + i * BleDataPool::kSlotSize; + s_data_pool->in_use[i] = false; + } + return true; +} + +/** @brief OTA end/abort/断连时释放整个池。 + * 安全性:仅当所有槽均空闲时才释放;若 worker 仍有 in-flight batch + * 占用槽(in_use=true),跳过本次释放,留待下次 poolFree 全部空闲时 + * 再释放。这避免了 worker 访问已释放内存的竞态(单核抢占: + * session_reset 运行在 NimBLE host task,worker 是独立任务,二者仅 + * 通过队列交接,无显式同步)。 */ +static void poolFree() +{ + if (!s_data_pool) return; + for (size_t i = 0; i < BleDataPool::kSlots; ++i) { + if (s_data_pool->in_use[i]) { + // 有槽仍被 worker 占用,跳过释放(下次 poolFree 再试) + DEBUG_DBG(TAG, "DATA_IN 池延迟释放: 槽%u in_use", (unsigned)i); + return; + } + } + // buf[0] 是连续 blob 的起始(一次 calloc 分配) + if (s_data_pool->buf[0]) { + free(s_data_pool->buf[0]); + } + free(s_data_pool); + s_data_pool = nullptr; +} + +/** @brief 从池借一个槽,返回索引;无空闲或池未分配返回 -1。仅 DATA_IN(host task)调用。 */ +static int poolAcquire() +{ + if (!s_data_pool) return -1; + for (size_t i = 0; i < BleDataPool::kSlots; ++i) { + if (!s_data_pool->in_use[i]) { + s_data_pool->in_use[i] = true; + return (int)i; + } + } + return -1; +} + +/** @brief 归还槽到池。仅 worker / drain(队列消费侧)调用。 */ +static void poolRelease(int idx) +{ + if (!s_data_pool) return; + if (idx >= 0 && idx < (int)BleDataPool::kSlots) { + s_data_pool->in_use[idx] = false; + } +} + +/** @brief 借槽拷贝数据;返回槽索引或 -1(池满/未分配/包过大)。 */ +static int poolCopyIn(const uint8_t *src, size_t len) +{ + if (!s_data_pool || len > BleDataPool::kSlotSize) return -1; + int idx = poolAcquire(); + if (idx < 0) return -1; + memcpy(s_data_pool->buf[idx], src, len); + return idx; +} + +/** @brief 归还池槽(跨 TU 调用入口:drainOtaDataQueue / 会话清理)。 */ +void ble_ota_pool_release(int idx) +{ + poolRelease(idx); +} + // ============================================================ // DATA_IN 丢包负反馈:把 malloc 失败 / 队列满从"静默丢包"变成 // "主动发缺口 ACK 让前端按 completed_bytes 续传退避"。 @@ -394,6 +520,11 @@ esp_err_t ble_esp32_ota_begin(size_t total_size) BleOta::instance().m_esp32LastSeq = 0xFFFFU; // ★ 分配 reorder buffer(失败则自动回退丢弃策略) esp32_reorder_alloc(); + // ★ 分配 DATA_IN 缓冲池(失败则 DATA_IN 回退 per-pack malloc 路径) + if (!poolAlloc()) { + DEBUG_WARN(TAG, "DATA_IN 池分配失败,回退 malloc free=%u", + (unsigned)esp_get_free_heap_size()); + } // ★ BLE OTA 开始:降低 WiFi 射频占用 ble_ota_boost_wifi_ps(); /* 刷写前再要一次高速连接参数(连接时若未谈妥,此处补刀) */ @@ -439,6 +570,8 @@ void ble_ota_session_reset() BleOta::instance().m_esp32RebootPending = false; // 释放 ESP32 reorder buffer(无 buffer 时为空操作) esp32_reorder_free(); + // 释放 DATA_IN 缓冲池(非 OTA 期间零占用,恢复堆基线) + poolFree(); // 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始 s_gd32_ooo_count = 0; s_ble_gd32_meta = {}; @@ -697,28 +830,41 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN; } - // 将数据拷贝到堆上并送入工作队列,避免在 NimBLE host task 中执行 + // 将数据拷贝到队列缓冲并送入工作线程,避免在 NimBLE host task 中执行 // 耗时的 flash 操作(esp_ota_begin / esp_ota_write)导致 BLE 连接断开。 + // ★ 优先走静态池(消除高频 malloc/free 堆抖动);池满回退 malloc + 负 ACK。 if (s_otaDataQueue) { // 解析 cmd 字节供负 ACK 区分 ESP32/GD32 续传分支(与 worker 解析一致) uint8_t cmd = parse_cmd_from_batch(buf, (size_t)recv_len); - uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len); - if (heap_buf) { - memcpy(heap_buf, buf, (size_t)recv_len); - BleOtaDataBatch item = { heap_buf, (size_t)recv_len }; + int pool_idx = poolCopyIn(buf, (size_t)recv_len); + if (pool_idx >= 0) { + BleOtaDataBatch item = { s_data_pool->buf[pool_idx], (size_t)recv_len, pool_idx }; if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) { - // 队列满:发缺口 ACK 让前端按 completed_bytes 续传退避, + // 队列满:归还池槽 + 发缺口 ACK 让前端按 completed_bytes 续传退避, // 而非等满 8s 窗口超时后整窗口重发 → 重传风暴叠加低堆二次失败 s_ota_drop_queue_full += 1; + poolRelease(pool_idx); DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len); - free(heap_buf); sendNegativeAckIfDue(cmd); } } else { - // malloc 失败:同上,发缺口 ACK 形成可恢复协议反馈,避免静默丢包 - s_ota_drop_malloc_fail += 1; - DEBUG_ERR(TAG, "DATA_IN 堆内存不足 (%d 字节),丢弃", recv_len); - sendNegativeAckIfDue(cmd); + // 池满或包过大:回退 malloc(保留原负 ACK 语义,避免静默丢包) + uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len); + if (heap_buf) { + memcpy(heap_buf, buf, (size_t)recv_len); + BleOtaDataBatch item = { heap_buf, (size_t)recv_len, -1 }; + if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) { + s_ota_drop_queue_full += 1; + free(heap_buf); + DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len); + sendNegativeAckIfDue(cmd); + } + } else { + // malloc 失败:同上,发缺口 ACK 形成可恢复协议反馈,避免静默丢包 + s_ota_drop_malloc_fail += 1; + DEBUG_ERR(TAG, "DATA_IN 堆内存不足 (%d 字节),丢弃", recv_len); + sendNegativeAckIfDue(cmd); + } } } @@ -1096,7 +1242,8 @@ void ble_ota_data_worker_task(void *pvParameters) BleOtaDataBatch item; if (xQueueReceive(s_otaDataQueue, &item, portMAX_DELAY) == pdTRUE) { if (!item.data || item.len == 0) { - free(item.data); + if (item.pool_idx >= 0) poolRelease(item.pool_idx); + else free(item.data); continue; } @@ -1104,12 +1251,14 @@ void ble_ota_data_worker_task(void *pvParameters) if (!BleOta::instance().isConnected() || BleOta::instance().isSuspendedForWifiOta()) { DEBUG_DBG(TAG, "BLE不可用 丢%zuB队列", item.len); - free(item.data); + if (item.pool_idx >= 0) poolRelease(item.pool_idx); + else free(item.data); continue; } ble_ota_process_data_batch(item.data, item.len); - free(item.data); + if (item.pool_idx >= 0) poolRelease(item.pool_idx); + else free(item.data); } } } diff --git a/main/server/ble_ota_host.cpp b/main/server/ble_ota_host.cpp index bc647a0..58fd7c9 100644 --- a/main/server/ble_ota_host.cpp +++ b/main/server/ble_ota_host.cpp @@ -227,7 +227,8 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, BleOtaDataBatch stale; size_t dropped = 0; while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) { - free(stale.data); + if (stale.pool_idx >= 0) ble_ota_pool_release(stale.pool_idx); + else free(stale.data); dropped++; } if (dropped > 0) { diff --git a/main/server/ble_ota_priv.h b/main/server/ble_ota_priv.h index 3dd667b..3fbdd29 100644 --- a/main/server/ble_ota_priv.h +++ b/main/server/ble_ota_priv.h @@ -61,6 +61,7 @@ struct BleOtaDataBatch { uint8_t *data; size_t len; + int pool_idx; ///< 静态池槽索引(>=0 为池内,<0 为 heap malloc,需 free) }; struct BleGd32RelayWaitCtx { @@ -87,6 +88,8 @@ extern bool s_wifi_ps_boosted; extern bool s_conn_fallback_requested; void drainOtaDataQueue(); +/** @brief 归还 DATA_IN 静态池槽(drainOtaDataQueue / 会话清理跨 TU 调用)。 */ +void ble_ota_pool_release(int idx); void ble_ota_start_advertising(void); void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); void sendFinishEvent(uint8_t stage, uint32_t value); diff --git a/main/server/http_handlers.cpp b/main/server/http_handlers.cpp index 9ca671d..5ed4b0b 100644 --- a/main/server/http_handlers.cpp +++ b/main/server/http_handlers.cpp @@ -19,10 +19,11 @@ #include "esp_heap_caps.h" #include "esp_rom_sys.h" #include "soc/soc.h" -#include "cJSON.h" #include "app_config.h" #include "http_handlers.h" +#include "http_util.h" +#include "json_parse.h" #include "wifi_ota.h" #include "esp32_ota_core.h" #include "ota_coordinator.h" @@ -42,25 +43,6 @@ #include "ble_ota.h" #include "system_boot.h" -static size_t parseHexToBytes(const char *hex, uint8_t *out, size_t max_out) -{ - size_t hex_len = strlen(hex); - size_t n = 0; - for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) { - char byte_str[3] = { hex[i], hex[i + 1], 0 }; - out[n++] = (uint8_t)strtol(byte_str, NULL, 16); - } - return n; -} - -static void bytesToHexUpper(const uint8_t *in, size_t n, char *out) -{ - for (size_t i = 0; i < n; ++i) { - snprintf(out + i * 2, 3, "%02X", in[i]); - } - out[n * 2] = '\0'; -} - /** * @brief 统一 JSON 错误响应(避免前端 resp.json() 被 httpd 明文坑)。 * @param http_status 如 "400 Bad Request" / "403 Forbidden" / "500 Internal Server Error" @@ -138,23 +120,18 @@ esp_err_t esp32_ota_update_handler(httpd_req_t *req) int ret = httpd_req_recv(req, buf, req->content_len); if (ret > 0) { buf[ret] = '\0'; - cJSON *root = cJSON_Parse(buf); - if (root) { - cJSON *sha_item = cJSON_GetObjectItem(root, "sha256"); - if (sha_item && cJSON_IsString(sha_item)) { - const char *sha_str = cJSON_GetStringValue(sha_item); - if (sha_str && strlen(sha_str) == 64) { - strncpy(frontend_sha_hex, sha_str, 64); - frontend_sha_hex[64] = '\0'; - has_frontend_sha = true; - DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex); - } + JsonView root = jsonParse(buf); + if (root.valid) { + JsonView sha_item = jsonGet(&root, "sha256"); + if (jsonIsString(&sha_item) && sha_item.str_len == 64) { + jsonGetStringCopy(&sha_item, frontend_sha_hex, 65); + has_frontend_sha = true; + DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex); } - cJSON *verify_item = cJSON_GetObjectItem(root, "verify_only"); - if (verify_item && cJSON_IsBool(verify_item)) { - verify_only = cJSON_IsTrue(verify_item); + JsonView verify_item = jsonGet(&root, "verify_only"); + if (jsonIsBool(&verify_item)) { + verify_only = jsonIsTrue(&verify_item); } - cJSON_Delete(root); } } } @@ -292,18 +269,16 @@ esp_err_t ota_upload_handler(httpd_req_t *req) if (pre_read_len > 256) pre_read_len = 256; memcpy(pre_read_buf, json_buf, pre_read_len); - cJSON *root = cJSON_Parse(json_buf); - if (root) { - cJSON *type_item = cJSON_GetObjectItem(root, "type"); - if (type_item && cJSON_IsString(type_item) && - strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) { + JsonView root = jsonParse(json_buf); + if (root.valid) { + JsonView type_item = jsonGet(&root, "type"); + if (jsonStringEquals(&type_item, "esp32")) { is_esp32_ota = true; } - cJSON *size_item = cJSON_GetObjectItem(root, "total_size"); - if (size_item) { - total_size = (uint32_t)cJSON_GetNumberValue(size_item); + JsonView size_item = jsonGet(&root, "total_size"); + if (size_item.valid) { + total_size = (uint32_t)jsonGetNumberValue(&size_item); } - cJSON_Delete(root); } } } @@ -564,15 +539,12 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req) int ret = httpd_req_recv(req, buf, req->content_len); if (ret > 0) { buf[ret] = '\0'; - cJSON *root = cJSON_Parse(buf); - if (root) { - cJSON *sha_item = cJSON_GetObjectItem(root, "sha256"); - if (sha_item && cJSON_IsString(sha_item) && - strlen(cJSON_GetStringValue(sha_item)) == 64) { - strncpy(frontend_sha_hex, cJSON_GetStringValue(sha_item), 64); - frontend_sha_hex[64] = '\0'; + JsonView root = jsonParse(buf); + if (root.valid) { + JsonView sha_item = jsonGet(&root, "sha256"); + if (jsonIsString(&sha_item) && sha_item.str_len == 64) { + jsonGetStringCopy(&sha_item, frontend_sha_hex, 65); } - cJSON_Delete(root); } } } @@ -582,17 +554,12 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req) } uint8_t frontend_sha[32] = {0}; - for (int i = 0; i < 32; ++i) { - char hex[3] = { frontend_sha_hex[i*2], frontend_sha_hex[i*2+1], 0 }; - frontend_sha[i] = (uint8_t)strtoul(hex, nullptr, 16); - } + http_hex_to_bytes(frontend_sha_hex, frontend_sha, 32); uint8_t device_sha[32] = {0}; char device_sha_hex[65] = {0}; esp_err_t verify_err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha); - for (int i = 0; i < 32; ++i) { - snprintf(&device_sha_hex[i*2], 3, "%02X", device_sha[i]); - } + http_bytes_to_hex_upper(device_sha, 32, device_sha_hex); if (verify_err == ESP_ERR_INVALID_CRC) { DEBUG_ERR("OTA", "GD32 SHA-256 不匹配"); @@ -694,21 +661,16 @@ static SemaphoreHandle_t httpJsonWorkMutex(void) */ static constexpr uint8_t FEEDBACK_DATA_HEX_MAX = 64; +/* can_feedback 分块追加:溢出时返回 -1 让调用方 break 截断 entries。 + * 与 http_json_append 的区别仅在此处需要"溢出=break"而非"溢出=截断填满"。 */ static int feedback_append(char *buf, size_t cap, int off, const char *fmt, ...) { - if (!buf || cap == 0 || off < 0 || (size_t)off >= cap) { - return -1; - } + if (!buf || cap == 0 || off < 0 || (size_t)off >= cap) return -1; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf + off, cap - (size_t)off, fmt, ap); va_end(ap); - if (n < 0) { - return -1; - } - if (off + n >= (int)cap) { - return -1; /* 分块装不下:调用方截断 entries */ - } + if (n < 0 || off + n >= (int)cap) return -1; return off + n; } @@ -721,23 +683,15 @@ esp_err_t can_feedback_handler(httpd_req_t *req) * motor.html 的 onFeedback 只用 ch/enabled/valid/rx/tx/err/tec/rec/bus_state, * 不需要 entries(CAN 帧快照,index.html/can.html 才用)。 * 跳过 entries 省掉 N×snprintf+hex 编码 + chunked 发送,handler 从 ~10ms 降到 <1ms。 */ - bool want_lite = false; - { - char query[32]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[4] = {}; - if (httpd_query_key_value(query, "lite", val, sizeof(val)) == ESP_OK) { - want_lite = (val[0] == '1' || val[0] == 'y' || val[0] == 't'); - } - } - } + bool want_lite = http_query_bool(req, "lite"); - can_feedback_result_t results[CAN_CHANNEL_MAX] = {}; - bool valid[CAN_CHANNEL_MAX] = {}; - for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { - valid[i] = getLatestCanFeedback(i, &results[i]); - } + /* OTA / send_batch 期间 SPI 被独占:跳过 0xC1 查询,全部通道返回 valid=false。 + * 响应结构不变,前端轮询自然显示 "--",恢复后自动回来。 */ + const bool spi_paused = canFeedbackPaused(); + /* 2026-08 架构改造:不再读被动缓存 s_latest,改为按需向 GD32 查询 0xC1。 + * lite 路径只需 page 0(统计在 page0 header);full 路径需 page 0+1(entries)。 + * 单通道 result 在栈上用完即释放,不再常驻 s_latest[3]/s_work[3](省 ~7.8KB BSS)。 */ const bool feat_known = SpiMaster::instance().gd32FeatKnown(); const uint32_t feat = SpiMaster::instance().gd32FeatFlags(); const bool ch_en[CAN_CHANNEL_MAX] = { @@ -750,7 +704,8 @@ esp_err_t can_feedback_handler(httpd_req_t *req) : 0x7U; /* lite 路径:一次性小响应,不走 chunked,不需要 httpJsonWorkMutex(用栈缓冲即可)。 - * 体积上限:3 通道 × ~80B + header ~60B ≈ 300B,远小于 1024 栈预算。 */ + * 体积上限:3 通道 × ~80B + header ~60B ≈ 300B,远小于 1024 栈预算。 + * 每通道 1 次 SPI 查询(page 0),3 通道 ~6-12ms。 */ if (want_lite) { char resp[512]; int off = snprintf(resp, sizeof(resp), @@ -763,9 +718,15 @@ esp_err_t can_feedback_handler(httpd_req_t *req) return ESP_FAIL; } for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { - const can_feedback_result_t &r = results[i]; + can_feedback_result_t result = {}; + bool valid = false; + if (ch_en[i] && !spi_paused) { + uint8_t entryIdx = 0; + valid = (canFdQueryFeedback(i, 0, &result, &entryIdx) == 0); + } + const can_feedback_result_t &r = result; const char *stats_fmt; - if (valid[i] && ch_en[i]) { + if (valid) { stats_fmt = ",\"mode\":%u,\"seq\":%lu,\"rx\":%lu,\"tx\":%lu," "\"drop\":%lu,\"err\":%lu,\"tec\":%u,\"rec\":%u," "\"bus_state\":%u,\"count\":%u"; @@ -778,13 +739,13 @@ esp_err_t can_feedback_handler(httpd_req_t *req) i == 0 ? "" : ",", (unsigned)i, ch_en[i] ? "true" : "false", - valid[i] ? "true" : "false"); + valid ? "true" : "false"); if (n < 0 || off + n >= (int)sizeof(resp)) { httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "fb lite ch"); return ESP_FAIL; } off += n; - if (valid[i] && ch_en[i]) { + if (valid) { n = snprintf(resp + off, sizeof(resp) - (size_t)off, stats_fmt, (unsigned)r.stats.mode, @@ -821,7 +782,8 @@ esp_err_t can_feedback_handler(httpd_req_t *req) return httpd_resp_send(req, resp, off); } - /* 完整路径(含 entries):保留给 index.html / can.html 的 CAN 监控页 */ + /* 完整路径(含 entries):保留给 index.html / can.html 的 CAN 监控页 + * 每通道 2 次 SPI 查询(page 0+1),3 通道 6 次 ~12-24ms。 */ SemaphoreHandle_t mux = httpJsonWorkMutex(); if (mux && xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) { @@ -853,19 +815,31 @@ esp_err_t can_feedback_handler(httpd_req_t *req) } for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { - const can_feedback_result_t &r = results[i]; + /* 按需查询:page 0 重置 + 统计,page 1 追加 entries(OTA 暂停期间跳过) */ + can_feedback_result_t result = {}; + bool valid = false; + if (ch_en[i] && !spi_paused) { + uint8_t entryIdx = 0; + int rc0 = canFdQueryFeedback(i, 0, &result, &entryIdx); + if (rc0 == 0) { + valid = true; + /* page 1 追加剩余 entries(失败则仅用 page 0 的条目) */ + (void)canFdQueryFeedback(i, 1, &result, &entryIdx); + } + } + const can_feedback_result_t &r = result; int off = 0; off = feedback_append(work, HTTP_JSON_WORK_CAP, off, "%s{\"ch\":%u,\"enabled\":%s,\"valid\":%s", i == 0 ? "" : ",", (unsigned)i, ch_en[i] ? "true" : "false", - valid[i] ? "true" : "false"); + valid ? "true" : "false"); if (off < 0) { break; } - if (valid[i] && ch_en[i]) { + if (valid) { off = feedback_append(work, HTTP_JSON_WORK_CAP, off, ",\"mode\":%u,\"seq\":%lu,\"rx\":%lu,\"tx\":%lu," "\"drop\":%lu,\"err\":%lu,\"tec\":%u,\"rec\":%u," @@ -959,14 +933,29 @@ esp_err_t can_mode_handler(httpd_req_t *req) if (req->method == HTTP_GET) { http_set_keepalive_hdr(req); /* GET 轮询复用 TCP;POST 设置模式不复用 */ + + /* OTA / send_batch 期间不占 SPI:跳过 0xC1 查询与 0xC2 fallback */ + if (canFeedbackPaused()) { + char resp[64]; + int len = snprintf(resp, sizeof(resp), + "{\"mode\":0,\"source\":\"paused\",\"ok\":false}"); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, len); + return ESP_OK; + } + + /* 2026-08 架构改造:按需向 GD32 查询 0xC1 page0 取 mode(不再读缓存 s_latest)。 + * page0 header 含 mode 字段,1 次 SPI 查询 ~2-4ms。 */ can_feedback_result_t fb{}; - if (getLatestCanFeedback(0, &fb)) { + uint8_t entryIdx = 0; + if (canFdQueryFeedback(0, 0, &fb, &entryIdx) == 0) { char resp[64]; int len = snprintf(resp, sizeof(resp), "{\"mode\":%u}", (unsigned)fb.stats.mode); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); } else { + /* 查询失败:回退实时 SPI 0xC2 查询实际模式 */ can_rx_mode_t actual = CAN_RX_MODE_ALL_PASS; int rc = canFdSetRxMode(CAN_RX_MODE_ALL_PASS, &actual, 2000); char resp[64]; @@ -987,21 +976,19 @@ esp_err_t can_mode_handler(httpd_req_t *req) } buf[ret] = '\0'; - cJSON *root = cJSON_Parse(buf); - if (!root) { + JsonView root = jsonParse(buf); + if (!root.valid) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败"); return ESP_FAIL; } - cJSON *mode_item = cJSON_GetObjectItem(root, "mode"); - if (!mode_item || !cJSON_IsNumber(mode_item)) { - cJSON_Delete(root); + JsonView mode_item = jsonGet(&root, "mode"); + if (!jsonIsNumber(&mode_item)) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 mode 字段"); return ESP_FAIL; } - int requested = (int)cJSON_GetNumberValue(mode_item); - cJSON_Delete(root); + int requested = (int)jsonGetNumberValue(&mode_item); can_rx_mode_t target = CAN_RX_MODE_ALL_PASS; if (requested == 1) { @@ -1020,23 +1007,84 @@ esp_err_t can_mode_handler(httpd_req_t *req) httpd_resp_send(req, resp, len); return ESP_OK; } +/** + * @brief POST /can/send_batch 流式 body 读取器。 + * + * 用 512B 滚动缓冲逐字符读 HTTP body,避免一次性 malloc(≤49KB)。 + * 仅在 can_send_batch_handler 内使用,针对该端点的固定 JSON schema。 + */ +class BatchBodyReader { +public: + static constexpr size_t kBuf = 512; + + BatchBodyReader(httpd_req_t *req, int content_len) + : m_req(req), m_remaining(content_len > 0 ? content_len : 0) {} + + /** 读下一字节,EOF 返回 -1 */ + int nextChar() { + if (m_pos >= m_len && !refill()) return -1; + return (unsigned char)m_buf[m_pos++]; + } + /** 偷看下一字节(不推进),EOF 返回 -1 */ + int peekChar() { + if (m_pos >= m_len && !refill()) return -1; + return (unsigned char)m_buf[m_pos]; + } + +private: + httpd_req_t *m_req; + int m_remaining; + char m_buf[kBuf]; + size_t m_len = 0; + size_t m_pos = 0; + bool m_eof = false; + + bool refill() { + if (m_eof) return false; + if (m_pos > 0) { + if (m_pos < m_len) { + memmove(m_buf, m_buf + m_pos, m_len - m_pos); + m_len -= m_pos; + } else { + m_len = 0; + } + m_pos = 0; + } + int to_read = (int)kBuf - (int)m_len; + if (to_read > m_remaining) to_read = m_remaining; + if (to_read <= 0) { m_eof = true; return false; } + int ret = httpd_req_recv(m_req, m_buf + m_len, (size_t)to_read); + if (ret <= 0) { m_eof = true; return false; } + m_len += (size_t)ret; + m_remaining -= ret; + return m_pos < m_len; + } +}; + /** * @brief POST /can/send_batch — CAN 发送唯一入口(单帧 = frames 长度 1)。 * * JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] } - * 返回: { "status":"ok", "results":[ ... ], 可选 skipped/abort_* } - * 单帧 canFdSend 超时 2000ms(等应答);多帧 80ms/帧 + 帧间隔。 + * 返回: { "status":"ok","results":[ ... ], 可选 skipped/abort_* } + * + * 流式改造(2026-08):原一次性 malloc(≤49KB) + JSON parse 双份堆占 + + * 输出,峰值 ~100KB 会挤压 lwip pbuf 导致网络断连。 + * 现用 512B 滚动缓冲逐字符读 body,单帧 jsonParse(零堆分配) 即用即弃, + * 响应走 httpd_resp_send_chunk 小栈缓冲,峰值堆 <2KB。 + * + * 单帧 canFdSend 超时 1200ms(等应答);多帧 80ms/帧 + 帧间隔。 * 发送期间暂停 CAN 反馈任务。 */ esp_err_t can_send_batch_handler(httpd_req_t *req) { http_set_cors_headers(req); - constexpr int kMaxBody = 49152; constexpr int kMaxFrames = 128; constexpr uint32_t kPerFrameTimeoutMs = 80U; constexpr uint32_t kSingleFrameTimeoutMs = 1200U; /* 正常 <20ms;httpd 单线程,此值决定其它端点最坏阻塞 */ constexpr uint32_t kInterFrameDelayMs = 1U; + /* 上限保留 49152 以维持 API 兼容;流式读取不再一次性 malloc */ + constexpr int kMaxBody = 49152; int content_len = req->content_len; if (content_len <= 0 || content_len > kMaxBody) { @@ -1044,167 +1092,241 @@ esp_err_t can_send_batch_handler(httpd_req_t *req) "body 长度无效或过大"); } - char *buf = static_cast(malloc(static_cast(content_len) + 1U)); - if (!buf) { - return http_send_json_error(req, "500 Internal Server Error", "oom", "内存不足"); - } + BatchBodyReader reader(req, content_len); - int received = 0; - while (received < content_len) { - int ret = httpd_req_recv(req, buf + received, content_len - received); - if (ret <= 0) { - free(buf); - return http_send_json_error(req, "400 Bad Request", "recv_fail", "接收数据失败"); - } - received += ret; - } - buf[received] = '\0'; - - cJSON *root = cJSON_Parse(buf); - free(buf); - buf = nullptr; - if (!root) { - return http_send_json_error(req, "400 Bad Request", "json_parse", "JSON 解析失败"); - } - - cJSON *ch_item = cJSON_GetObjectItem(root, "ch"); - cJSON *frames = cJSON_GetObjectItem(root, "frames"); - if (!ch_item || !cJSON_IsArray(frames)) { - cJSON_Delete(root); - return http_send_json_error(req, "400 Bad Request", "bad_args", "需要 ch 与 frames 数组"); - } - - can_channel_t ch = static_cast(cJSON_GetNumberValue(ch_item)); - const int frame_count = cJSON_GetArraySize(frames); - if (frame_count <= 0 || frame_count > kMaxFrames) { - cJSON_Delete(root); - return http_send_json_error(req, "400 Bad Request", "bad_frames", - "frames 数量无效(1~128)"); - } - const uint32_t frame_timeout_ms = - (frame_count == 1) ? kSingleFrameTimeoutMs : kPerFrameTimeoutMs; - - cJSON *out_root = cJSON_CreateObject(); - cJSON *results = cJSON_CreateArray(); - if (out_root == nullptr || results == nullptr) { - cJSON_Delete(out_root); - cJSON_Delete(results); - cJSON_Delete(root); - return http_send_json_error(req, "500 Internal Server Error", "oom", "JSON 分配失败"); - } - cJSON_AddStringToObject(out_root, "status", "ok"); - cJSON_AddItemToObject(out_root, "results", results); - - /* 发送期间暂停 CAN 反馈,独占 SPI 业务路径 */ - pauseCanFeedbackTask(true); - - auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) { - if (!item) { - return; - } - cJSON_AddStringToObject(item, "status", "error"); - cJSON_AddNumberToObject(item, "code", code); - cJSON_AddNumberToObject(item, "gd32_err", gd32_err); - }; - - for (int i = 0; i < frame_count; i++) { - (void)esp_task_wdt_reset(); - - cJSON *item = cJSON_CreateObject(); - if (item == nullptr) { - break; - } - cJSON_AddItemToArray(results, item); - - cJSON *fr = cJSON_GetArrayItem(frames, i); - if (!cJSON_IsObject(fr)) { - fill_err(item, -1, 0); - continue; - } - - cJSON *id_item = cJSON_GetObjectItem(fr, "id"); - cJSON *data_item = cJSON_GetObjectItem(fr, "data"); - cJSON *dlc_item = cJSON_GetObjectItem(fr, "dlc"); - if (!id_item) { - fill_err(item, -1, 0); - continue; - } - - uint32_t ext_id = static_cast(cJSON_GetNumberValue(id_item)); - uint8_t dlc = dlc_item ? static_cast(cJSON_GetNumberValue(dlc_item)) : 0U; - if (dlc > CAN_MAX_DLC) { - dlc = CAN_MAX_DLC; - } - uint8_t tx_data[CAN_MAX_DLC]; - memset(tx_data, 0, sizeof(tx_data)); - uint8_t tx_dlc = 0U; - if (data_item && cJSON_IsString(data_item)) { - const char *hex = cJSON_GetStringValue(data_item); - if (hex) { - tx_dlc = static_cast(parseHexToBytes(hex, tx_data, CAN_MAX_DLC)); + /* ── prefix 解析:定位 "ch" → ':' → 数字;再定位 "frames" → '[' ── */ + auto findKey = [&](const char *key) -> bool { + const size_t kl = strlen(key); + size_t match = 0; + int c; + while ((c = reader.nextChar()) != -1) { + if (c == key[match]) { + if (++match == kl) return true; + } else { + match = (c == key[0]) ? 1 : 0; } } - if (tx_dlc == 0U && dlc > 0U) { - tx_dlc = dlc; + return false; + }; + auto skipWs = [&]() { + int c; + while ((c = reader.peekChar()) != -1) { + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { + reader.nextChar(); + } else { + break; + } + } + }; + auto findChar = [&](char target) -> bool { + int c; + while ((c = reader.nextChar()) != -1) { + if (c == target) return true; + } + return false; + }; + auto readInt = [&]() -> long { + skipWs(); + long sign = 1; + int c = reader.peekChar(); + if (c == '-') { reader.nextChar(); sign = -1; } + long v = 0; + while ((c = reader.peekChar()) != -1 && c >= '0' && c <= '9') { + v = v * 10 + (c - '0'); + reader.nextChar(); + } + return v * sign; + }; + + if (!findKey("\"ch\"")) { + return http_send_json_error(req, "400 Bad Request", "bad_args", "缺少 ch 字段"); + } + if (!findChar(':')) { + return http_send_json_error(req, "400 Bad Request", "bad_args", "ch 字段格式错误"); + } + long ch_val = readInt(); + if (!findKey("\"frames\"")) { + return http_send_json_error(req, "400 Bad Request", "bad_args", "缺少 frames 字段"); + } + if (!findChar('[')) { + return http_send_json_error(req, "400 Bad Request", "bad_args", "frames 非数组"); + } + can_channel_t ch = static_cast(ch_val); + + /* ── 读取单个帧对象到大括号匹配,返回含外层 {} 的 JSON 子串 ── */ + char frameJson[320]; + auto readFrameObj = [&]() -> int { + skipWs(); + int c = reader.peekChar(); + if (c != '{') return -1; + reader.nextChar(); /* 消费 '{' */ + frameJson[0] = '{'; + size_t n = 1; + int depth = 1; + while ((c = reader.nextChar()) != -1) { + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + if (n + 1 >= sizeof(frameJson)) return -1; + frameJson[n++] = '}'; + frameJson[n] = '\0'; + return (int)n; + } + } + if (n + 1 >= sizeof(frameJson)) return -1; + frameJson[n++] = (char)c; + } + return -1; + }; + + /* abort 信息(processFrame 命中致命错误时填写) */ + int abort_code = 0; + int abort_gd32_err = 0; + int emitted = 0; + char chunk[192]; + /* 单帧/多帧超时在读完首帧后确定;lambda 按引用捕获,后续赋值生效 */ + uint32_t frame_timeout_ms = kPerFrameTimeoutMs; + + /* ── 处理一帧:解析 JSON → canFdSend → 发 chunk ── + * 返回 true 表示致命错误需中止后续帧。 */ + auto processFrame = [&]() -> bool { + JsonView fr = jsonParse(frameJson); + if (!fr.valid) { + int n = snprintf(chunk, sizeof(chunk), + "%s{\"status\":\"error\",\"code\":-1,\"gd32_err\":0}", + (emitted == 0) ? "" : ","); + httpd_resp_send_chunk(req, chunk, n); + emitted++; + return false; + } + JsonView id_item = jsonGet(&fr, "id"); + JsonView data_item = jsonGet(&fr, "data"); + JsonView dlc_item = jsonGet(&fr, "dlc"); + if (!id_item.valid) { + int n = snprintf(chunk, sizeof(chunk), + "%s{\"status\":\"error\",\"code\":-1,\"gd32_err\":0}", + (emitted == 0) ? "" : ","); + httpd_resp_send_chunk(req, chunk, n); + emitted++; + return false; } - uint8_t resp_data[CAN_MAX_DLC]; - memset(resp_data, 0, sizeof(resp_data)); + uint32_t ext_id = static_cast(jsonGetNumberValue(&id_item)); + uint8_t dlc = dlc_item.valid ? static_cast(jsonGetNumberValue(&dlc_item)) : 0U; + if (dlc > CAN_MAX_DLC) dlc = CAN_MAX_DLC; + uint8_t tx_data[CAN_MAX_DLC] = {0}; + uint8_t tx_dlc = 0U; + if (jsonIsString(&data_item)) { + size_t hex_len = 0; + const char *hex = jsonGetStringValue(&data_item, &hex_len); + if (hex) { + tx_dlc = static_cast(http_hex_to_bytes_n(hex, hex_len, tx_data, CAN_MAX_DLC)); + } + } + if (tx_dlc == 0U && dlc > 0U) tx_dlc = dlc; + + uint8_t resp_data[CAN_MAX_DLC] = {0}; uint8_t gd32_err = 0U; int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc, resp_data, sizeof(resp_data), frame_timeout_ms, &gd32_err); if (resp_len >= 0) { - char resp_hex[CAN_MAX_DLC * 2 + 1]; - memset(resp_hex, 0, sizeof(resp_hex)); + char resp_hex[CAN_MAX_DLC * 2 + 1] = {0}; const size_t copy_n = (static_cast(resp_len) < sizeof(resp_data)) ? static_cast(resp_len) : sizeof(resp_data); - bytesToHexUpper(resp_data, copy_n, resp_hex); - cJSON_AddStringToObject(item, "status", "ok"); - cJSON_AddNumberToObject(item, "sent", tx_dlc); - cJSON_AddNumberToObject(item, "recv_len", static_cast(copy_n)); - cJSON_AddStringToObject(item, "recv", resp_hex); - } else { - fill_err(item, resp_len, gd32_err); - /* SPI 失败 / BUS_OFF / TEC 过高:中止剩余帧,由 skipped 字段告知前端 */ - const bool fatal_spi = (resp_len == -2); - const bool fatal_bus = (resp_len == -3) && - (gd32_err == 0xE2U || gd32_err == 0xE3U); - if (fatal_spi || fatal_bus) { - const int skipped = frame_count - i - 1; - if (skipped > 0) { - cJSON_AddNumberToObject(out_root, "skipped", skipped); - cJSON_AddNumberToObject(out_root, "abort_code", resp_len); - cJSON_AddNumberToObject(out_root, "abort_gd32_err", gd32_err); - } - break; - } + http_bytes_to_hex_upper(resp_data, copy_n, resp_hex); + int n = snprintf(chunk, sizeof(chunk), + "%s{\"status\":\"ok\",\"sent\":%u,\"recv_len\":%u,\"recv\":\"%s\"}", + (emitted == 0) ? "" : ",", + (unsigned)tx_dlc, (unsigned)copy_n, resp_hex); + httpd_resp_send_chunk(req, chunk, n); + emitted++; + return false; } + /* 错误项 */ + int n = snprintf(chunk, sizeof(chunk), + "%s{\"status\":\"error\",\"code\":%d,\"gd32_err\":%u}", + (emitted == 0) ? "" : ",", resp_len, (unsigned)gd32_err); + httpd_resp_send_chunk(req, chunk, n); + emitted++; + /* SPI 失败 / BUS_OFF / TEC 过高:中止剩余帧,由 skipped 字段告知前端 */ + const bool fatal_spi = (resp_len == -2); + const bool fatal_bus = (resp_len == -3) && (gd32_err == 0xE2U || gd32_err == 0xE3U); + if (fatal_spi || fatal_bus) { + abort_code = resp_len; + abort_gd32_err = (int)gd32_err; + return true; + } + return false; + }; - if (i + 1 < frame_count) { + /* ── 先读第一帧,判断单帧/多帧以决定超时 ── */ + int flen = readFrameObj(); + if (flen <= 0) { + /* 空数组或首帧非法:直接返回空 results */ + httpd_resp_set_type(req, "application/json"); + static const char kEmptyResults[] = "{\"status\":\"ok\",\"results\":[]}"; + httpd_resp_send_chunk(req, kEmptyResults, sizeof(kEmptyResults) - 1); + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; + } + + /* 偷看第一帧之后是否为 ']':是→单帧,否→多帧 */ + skipWs(); + const bool single_frame = (reader.peekChar() == ']'); + frame_timeout_ms = single_frame ? kSingleFrameTimeoutMs : kPerFrameTimeoutMs; + + /* ── 开始 chunked 响应(prefix 已校验完毕,不会再回退为 error) ── */ + httpd_resp_set_type(req, "application/json"); + static const char kResultsPrefix[] = "{\"status\":\"ok\",\"results\":["; + httpd_resp_send_chunk(req, kResultsPrefix, sizeof(kResultsPrefix) - 1); + + pauseCanFeedbackTask(true); + + bool aborted = processFrame(); + int processed = 1; + + if (!aborted && !single_frame) { + while (processed < kMaxFrames) { + (void)esp_task_wdt_reset(); + skipWs(); + int c = reader.peekChar(); + if (c == -1 || c == ']') break; + if (c == ',') { reader.nextChar(); continue; } + if (c != '{') { reader.nextChar(); continue; } /* 跳过意外字符 */ + flen = readFrameObj(); + if (flen <= 0) break; + if (processFrame()) { aborted = true; break; } + processed++; vTaskDelay(pdMS_TO_TICKS(kInterFrameDelayMs)); } } pauseCanFeedbackTask(false); - cJSON_Delete(root); - root = nullptr; - char *printed = cJSON_PrintUnformatted(out_root); - cJSON_Delete(out_root); - out_root = nullptr; - if (!printed) { - static const char kOom[] = - "{\"status\":\"error\",\"code\":\"json_print\",\"message\":\"JSON输出失败\"}"; - httpd_resp_set_status(req, "500 Internal Server Error"); - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, kOom, HTTPD_RESP_USE_STRLEN); - return ESP_FAIL; + /* 统计中止后未发送的剩余帧数(按 '{' 计数到 ']') */ + int skipped = 0; + if (aborted) { + int c; + while ((c = reader.nextChar()) != -1) { + if (c == ']') break; + if (c == '{') skipped++; + } } - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, printed, HTTPD_RESP_USE_STRLEN); - free(printed); + char tail[96]; + int tn; + if (aborted && skipped > 0) { + tn = snprintf(tail, sizeof(tail), + "],\"skipped\":%d,\"abort_code\":%d,\"abort_gd32_err\":%u}", + skipped, abort_code, (unsigned)abort_gd32_err); + } else { + tn = snprintf(tail, sizeof(tail), "]}"); + } + httpd_resp_send_chunk(req, tail, tn); + httpd_resp_send_chunk(req, NULL, 0); return ESP_OK; } @@ -1219,35 +1341,37 @@ esp_err_t spi_send_handler(httpd_req_t *req) } buf[ret] = '\0'; - cJSON *root = cJSON_Parse(buf); - if (!root) { + JsonView root = jsonParse(buf); + if (!root.valid) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败"); return ESP_FAIL; } - cJSON *type_item = cJSON_GetObjectItem(root, "type"); - cJSON *data_item = cJSON_GetObjectItem(root, "data"); - cJSON *timeout_item = cJSON_GetObjectItem(root, "timeout_ms"); + JsonView type_item = jsonGet(&root, "type"); + JsonView data_item = jsonGet(&root, "data"); + JsonView timeout_item = jsonGet(&root, "timeout_ms"); - if (!type_item || !cJSON_IsString(type_item)) { - cJSON_Delete(root); + if (!jsonIsString(&type_item)) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 type 字段"); return ESP_FAIL; } - const char *type_hex = cJSON_GetStringValue(type_item); - uint8_t frame_type = (uint8_t)strtol(type_hex, NULL, 16); + /* type_hex 是 hex 字符串如 "A1",拷贝到小缓冲供 strtol 使用 */ + char type_hex_buf[8] = {}; + jsonGetStringCopy(&type_item, type_hex_buf, sizeof(type_hex_buf)); + uint8_t frame_type = (uint8_t)strtol(type_hex_buf, NULL, 16); - uint32_t timeout_ms = timeout_item ? (uint32_t)cJSON_GetNumberValue(timeout_item) : 1000; + uint32_t timeout_ms = timeout_item.valid ? (uint32_t)jsonGetNumberValue(&timeout_item) : 1000; uint8_t payload_data[SPI_OTA_FRAME_MAX] = {0}; size_t payload_len = 0; - if (data_item && cJSON_IsString(data_item)) { - payload_len = parseHexToBytes(cJSON_GetStringValue(data_item), payload_data, SPI_OTA_FRAME_MAX); + if (jsonIsString(&data_item)) { + size_t hex_len = 0; + const char *hex = jsonGetStringValue(&data_item, &hex_len); + payload_len = http_hex_to_bytes_n(hex, hex_len, payload_data, SPI_OTA_FRAME_MAX); } DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节", frame_type, payload_len); - cJSON_Delete(root); char resp[1024]; uint8_t resp_type = 0; @@ -1259,7 +1383,7 @@ esp_err_t spi_send_handler(httpd_req_t *req) if (spi_err == ESP_OK) { char resp_hex[SPI_OTA_FRAME_MAX * 2 + 1] = {0}; - bytesToHexUpper(resp_payload, resp_len, resp_hex); + http_bytes_to_hex_upper(resp_payload, resp_len, resp_hex); snprintf(resp, sizeof(resp), "{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}", frame_type, payload_len, resp_type, resp_len, resp_hex); @@ -1313,27 +1437,11 @@ static size_t get_task_stack_peak_used_bytes(TaskHandle_t handle, size_t stack_s return (stack_size_bytes >= high_watermark) ? (stack_size_bytes - high_watermark) : 0; } -static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...) -{ - if (!buf || cap == 0 || offset < 0 || (size_t)offset >= cap) return offset; - va_list args; - va_start(args, fmt); - int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, args); - va_end(args); - if (n < 0) return offset; - size_t written = (size_t)n; - if (offset + (int)written >= (int)cap) { - buf[cap - 1] = '\0'; - return (int)(cap - 1); - } - return offset + (int)written; -} - /** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样)。最多 16 条任务以免撑爆 3KB 分块。 */ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu) { static constexpr size_t kCpuTaskJsonMax = 16; - offset = append_jsonf(buf, cap, offset, + offset = http_json_append(buf, cap, offset, "\"cpu\":{\"available\":%s,\"valid\":%s,\"usage_pct\":%.2f,\"idle_pct\":%.2f,\"sample_ms\":%lu,\"tasks\":[", cpu.available ? "true" : "false", cpu.valid ? "true" : "false", @@ -1345,7 +1453,7 @@ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUs (cpu.task_count > kCpuTaskJsonMax) ? kCpuTaskJsonMax : cpu.task_count; for (size_t i = 0; i < ntask; ++i) { const CpuTaskMetric& task = cpu.tasks[i]; - offset = append_jsonf(buf, cap, offset, + offset = http_json_append(buf, cap, offset, "%s{\"name\":\"%s\",\"task_number\":%lu,\"runtime_delta\":%" PRIu64 ",\"usage_pct\":%.2f}", i == 0 ? "" : ",", task.name, @@ -1355,7 +1463,7 @@ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUs } // 不带尾随逗号;由后续拼接块自行添加前导逗号,避免双重逗号 - return append_jsonf(buf, cap, offset, "]}"); + return http_json_append(buf, cap, offset, "]}"); } esp_err_t system_status_handler(httpd_req_t *req) @@ -1368,16 +1476,7 @@ esp_err_t system_status_handler(httpd_req_t *req) * collectTaskStackMetrics / OtaCoordinator::getSnapshot / SystemBoot::appendStatusJson * 等重活,避免在 httpd 单线程里阻塞几十~上百 ms 拖慢 motor/feedback 请求。 * 完整 status 由前端 5s 拉一次。 */ - bool want_lite = false; - { - char query[32]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[4] = {}; - if (httpd_query_key_value(query, "lite", val, sizeof(val)) == ESP_OK) { - want_lite = (val[0] == '1' || val[0] == 'y' || val[0] == 't'); - } - } - } + bool want_lite = http_query_bool(req, "lite"); if (want_lite) { char ip_str[16] = "0.0.0.0"; const char *mode_str = "AP"; @@ -1413,17 +1512,7 @@ esp_err_t system_status_handler(httpd_req_t *req) /* layout=1:附带链接期静态布局(IRAM/.data/.bss/堆容量等),前端缓存后轮询不再要。 * 默认只下发 memory 动态字段,减 status JSON 体积。 */ - bool want_mem_layout = false; - { - char query[48]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[8] = {}; - if (httpd_query_key_value(query, "layout", val, sizeof(val)) == ESP_OK) { - want_mem_layout = (val[0] == '1' || val[0] == 'y' || val[0] == 'Y' || - val[0] == 't' || val[0] == 'T'); - } - } - } + bool want_mem_layout = http_query_bool(req, "layout"); /* 堆极紧时仍返回精简 JSON,禁止 500(前端轮询会刷红)。 */ const size_t largest_now = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); @@ -1625,7 +1714,7 @@ esp_err_t system_status_handler(httpd_req_t *req) case 2: role_str = "boot"; break; default: break; } - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, 0, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, 0, ",\"spi_link\":{\"version\":%u,\"gd32_role\":\"%s\",\"session_id\":\"0x%08lX\"," "\"v2_active\":%s,\"local_credit\":%u,\"remote_credit\":%u," "\"crc_errors\":%lu,\"duplicate_requests\":%lu,\"seq_errors\":%lu," @@ -1657,7 +1746,7 @@ esp_err_t system_status_handler(httpd_req_t *req) const char *en = feat_known ? (feat_adc ? "true" : "false") : (have ? "true" : "null"); if (have) { - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"gd32_adc\":{\"enabled\":%s,\"feat_known\":%s,\"fresh\":%s,\"valid\":%s," "\"temperature\":%.2f,\"vdda\":%.3f,\"vin_adc\":%.3f,\"vcc\":%.3f," "\"temp_raw\":%u,\"vref_raw\":%u,\"seq\":%lu," @@ -1670,12 +1759,12 @@ esp_err_t system_status_handler(httpd_req_t *req) (unsigned long)ga.seq, (unsigned long)ga.uptime_sec, (unsigned long)(age == UINT32_MAX ? 0UL : (unsigned long)age)); } else { - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"gd32_adc\":{\"enabled\":%s,\"feat_known\":%s,\"fresh\":false,\"valid\":false}", en, feat_known ? "true" : "false"); } const uint32_t fl = SpiMaster::instance().gd32FeatFlags(); - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"gd32_features\":{\"known\":%s,\"flags\":%lu," "\"adc_feedback\":%s,\"uart_temp_print\":%s," "\"spi_app_log\":%s,\"can_brs\":%s," @@ -1696,7 +1785,7 @@ esp_err_t system_status_handler(httpd_req_t *req) /* memory 动态字段:每次轮询;layout 仅 ?layout=1 时附带(链接期常量)。 */ if (want_mem_layout) { - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"memory\":{\"layout\":{\"chip\":\"esp32c3\",\"sram_total\":%lu,\"sram_soc\":%lu," "\"static_reserved\":%lu,\"reserved_tail\":%lu," "\"iram\":%lu,\"data\":%lu,\"bss\":%lu," @@ -1728,7 +1817,7 @@ esp_err_t system_status_handler(httpd_req_t *req) (unsigned long)rtc_free, (unsigned long)free_heap); } else { - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, ",\"memory\":{\"heap_used\":%lu,\"heap_free\":%lu," "\"heap_min_free\":%lu,\"heap_largest_free\":%lu," "\"alloc_blocks\":%lu,\"free_blocks\":%lu," @@ -1752,9 +1841,9 @@ esp_err_t system_status_handler(httpd_req_t *req) } /* ── 分块 4:task_stack + 闭合 ── */ - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, 0, ",\"task_stack\": {"); + len = http_json_append(resp, HTTP_JSON_WORK_CAP, 0, ",\"task_stack\": {"); for (size_t i = 0; i < task_metrics_count; ++i) { - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, "%s\"%s\":{\"stack_size_bytes\":%lu,\"high_watermark_bytes\":%lu,\"peak_used_bytes\":%lu}", i == 0 ? "" : ",", task_metrics[i].name, @@ -1762,7 +1851,7 @@ esp_err_t system_status_handler(httpd_req_t *req) (unsigned long)get_task_stack_high_watermark_bytes(task_metrics[i].handle), (unsigned long)get_task_stack_peak_used_bytes(task_metrics[i].handle, task_metrics[i].stack_size_bytes)); } - len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, "}}"); + len = http_json_append(resp, HTTP_JSON_WORK_CAP, len, "}}"); if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) { if (mux) { @@ -1870,16 +1959,7 @@ esp_err_t motor_state_handler(httpd_req_t *req) * rx_count u32, seq u32, data[32] * 27 槽(9 node × 3 ch,node×channel 双键)最大 12 + 27*44 = 1200B。 * 无 have_data 时仍发 header(count=0),让前端区分「无数据」与「空总线」。 */ - bool want_bin = false; - { - char query[32]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[4] = {}; - if (httpd_query_key_value(query, "bin", val, sizeof(val)) == ESP_OK) { - want_bin = (val[0] == '1' || val[0] == 'y' || val[0] == 't'); - } - } - } + bool want_bin = http_query_bool(req, "bin"); if (want_bin) { httpd_resp_set_type(req, "application/octet-stream"); /* 静态缓冲:27 槽扩容后 1200B 不再上栈(httpd 栈 7KB 已紧); @@ -1960,9 +2040,7 @@ esp_err_t motor_state_handler(httpd_req_t *req) /* data hex */ char hex[MOTOR_STATUS_DLC * 2 + 1]; - for (uint8_t b = 0; b < MOTOR_STATUS_DLC; ++b) { - snprintf(&hex[b * 2], 3, "%02X", s.data[b]); - } + http_bytes_to_hex_upper(s.data, MOTOR_STATUS_DLC, hex); off += snprintf(w + off, HTTP_JSON_WORK_CAP - off, "%s{\"node\":%u,\"ch\":%u,\"online\":%s,\"dlc\":%u," @@ -2027,9 +2105,7 @@ esp_err_t motor_version_handler(httpd_req_t *req) if (!s.valid || s.node_id == 0) continue; char hex[MOTORVER_DLC * 2 + 1]; - for (uint8_t b = 0; b < MOTORVER_DLC; ++b) { - snprintf(&hex[b * 2], 3, "%02X", s.version[b]); - } + http_bytes_to_hex_upper(s.version, MOTORVER_DLC, hex); const long age = (s.recv_ms > 0 && now_ms >= s.recv_ms) ? static_cast(now_ms - s.recv_ms) : 0L; @@ -2060,14 +2136,7 @@ esp_err_t motor_ota_upload_handler(httpd_req_t *req) { http_set_cors_headers(req); - char query[16] = {}; - uint8_t channel = 0; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[4] = {}; - if (httpd_query_key_value(query, "ch", val, sizeof(val)) == ESP_OK) { - channel = static_cast(atoi(val)); - } - } + uint8_t channel = static_cast(http_query_u32(req, "ch", 0)); const int content_len = req->content_len; if (content_len <= 0 || content_len > (int)MOTOR_OTA_MAX_FW_SIZE) { @@ -2164,9 +2233,7 @@ esp_err_t motor_ota_upload_handler(httpd_req_t *req) ota_meta_t sha_meta = {}; char sha_hex[65] = {0}; if (Gd32FlashStore::instance().metaLoad(&sha_meta) == ESP_OK) { - for (int i = 0; i < 32; ++i) { - snprintf(&sha_hex[i * 2], 3, "%02x", sha_meta.checksum[i]); - } + http_bytes_to_hex_lower(sha_meta.checksum, 32, sha_hex); } httpd_resp_set_type(req, "application/json"); @@ -2185,14 +2252,7 @@ esp_err_t motor_ota_relay_handler(httpd_req_t *req) http_set_keepalive_hdr(req); DEBUG_INFO("MOTOR_OTA", "=== HTTP: POST /motor/ota/relay ==="); - char query[16] = {}; - uint8_t channel = 0; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - char val[4] = {}; - if (httpd_query_key_value(query, "ch", val, sizeof(val)) == ESP_OK) { - channel = static_cast(atoi(val)); - } - } + uint8_t channel = static_cast(http_query_u32(req, "ch", 0)); /* 读 meta 拿 fw_size(motorOtaStart 参数的唯一来源)。 * total_size==0 = 无固件可刷(未上传过,或上次 relay 成功后 metaClear 已消费)。 diff --git a/main/server/http_util.h b/main/server/http_util.h new file mode 100644 index 0000000..a86be46 --- /dev/null +++ b/main/server/http_util.h @@ -0,0 +1,109 @@ +/* + * http_util.h - HTTP handler 共享工具(JSON 追加、query 解析、hex 编解码) + * + * 消除 http_handlers.cpp / health_monitor.cpp / spi_speed_test.cpp 中的重复样板。 + */ +#ifndef HTTP_UTIL_H +#define HTTP_UTIL_H + +#include +#include +#include +#include +#include "esp_http_server.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── JSON buffer 追加 ────────────────────────────────────────── + * 统一替代 feedback_append / append_jsonf / appendJsonfSafe 三份拷贝。 + * 截断时不越界,返回新 offset;出错返回 -1 或原 offset(视调用方约定)。 + * 返回 (int)cap-1 表示已截断填满。 */ +static inline int http_json_append(char *buf, size_t cap, int offset, const char *fmt, ...) +{ + if (!buf || cap == 0 || offset < 0 || (size_t)offset >= cap) return offset; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap); + va_end(ap); + if (n < 0) return offset; + if ((size_t)n >= cap - (size_t)offset) return (int)cap - 1; /* 截断 */ + return offset + n; +} + +/* ── Query 参数解析 ──────────────────────────────────────────── + * 统一替代 6 处内联 ?lite=1 / ?bin=1 / ?ch=N 解析块。 */ + +/** 查询布尔型参数:值为 "1"/"y"/"t"/"T" 时返回 true,缺失或其它返回 false。 */ +static inline bool http_query_bool(httpd_req_t *req, const char *key) +{ + char query[48]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) return false; + char val[8] = {}; + if (httpd_query_key_value(query, key, val, sizeof(val)) != ESP_OK) return false; + return val[0] == '1' || val[0] == 'y' || val[0] == 'Y' || + val[0] == 't' || val[0] == 'T'; +} + +/** 查询无符号整型参数:缺失/非法时返回 defval。 */ +static inline uint32_t http_query_u32(httpd_req_t *req, const char *key, uint32_t defval) +{ + char query[64]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) return defval; + char val[16] = {}; + if (httpd_query_key_value(query, key, val, sizeof(val)) != ESP_OK) return defval; + return (uint32_t)strtoul(val, NULL, 10); +} + +/* ── Hex 编解码 ──────────────────────────────────────────────── + * 统一替代内联 strtoul 循环和 snprintf("%02X") 循环。 */ + +/** hex 字符串 → 字节数组,返回写入字节数。 */ +static inline size_t http_hex_to_bytes(const char *hex, uint8_t *out, size_t max_out) +{ + size_t hex_len = strlen(hex); + size_t n = 0; + for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) { + char byte_str[3] = { hex[i], hex[i + 1], 0 }; + out[n++] = (uint8_t)strtol(byte_str, NULL, 16); + } + return n; +} + +/** hex 字符串(带长度)→ 字节数组,返回写入字节数。 + * 供 json_parse.h 的非 NUL 终止字符串值使用。 */ +static inline size_t http_hex_to_bytes_n(const char *hex, size_t hex_len, uint8_t *out, size_t max_out) +{ + size_t n = 0; + for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) { + char byte_str[3] = { hex[i], hex[i + 1], 0 }; + out[n++] = (uint8_t)strtol(byte_str, NULL, 16); + } + return n; +} + +/** 字节数组 → 大写 hex 字符串(out 需至少 2*n+1 字节)。 */ +static inline void http_bytes_to_hex_upper(const uint8_t *in, size_t n, char *out) +{ + for (size_t i = 0; i < n; ++i) { + snprintf(out + i * 2, 3, "%02X", in[i]); + } + out[n * 2] = '\0'; +} + +/** 字节数组 → 小写 hex 字符串(out 需至少 2*n+1 字节)。 */ +static inline void http_bytes_to_hex_lower(const uint8_t *in, size_t n, char *out) +{ + for (size_t i = 0; i < n; ++i) { + snprintf(out + i * 2, 3, "%02x", in[i]); + } + out[n * 2] = '\0'; +} + +#ifdef __cplusplus +} +#endif + +#endif // HTTP_UTIL_H \ No newline at end of file diff --git a/main/server/json_parse.h b/main/server/json_parse.h new file mode 100644 index 0000000..220cd43 --- /dev/null +++ b/main/server/json_parse.h @@ -0,0 +1,243 @@ +/* + * json_parse.h - 极简 JSON 扁平对象解析器(cJSON 零堆分配替代) + * + * 项目中对 JSON 的使用全部是"解析前端发来的扁平 JSON 对象": + * { "sha256": "...", "verify_only": true, "total_size": 12345, "type": "esp32" } + * 无嵌套对象、无数组(can_send_batch 已自研流式解析)。 + * 本解析器直接在调用方提供的可写字符串缓冲上做扫描, + * 不分配堆内存,字符串值通过就地 NUL 终止返回指针。 + * + * 用法: + * JsonView root = jsonParse(buf); // buf 须为可写 char[] + * if (!root.valid) return error; + * JsonView sha = jsonGet(&root, "sha256"); + * if (sha.type == JSON_STRING) { use(sha.str); } + * JsonView sz = jsonGet(&root, "total_size"); + * if (sz.type == JSON_NUMBER) { uint32_t n = (uint32_t)sz.num; } + * // 无需 free —— 指针指向 buf,buf 由调用方管理 + */ +#ifndef JSON_PARSE_H +#define JSON_PARSE_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + JSON_INVALID = 0, + JSON_NULL, + JSON_BOOL, + JSON_NUMBER, + JSON_STRING, + JSON_OBJECT, +} JsonType; + +typedef struct { + JsonType type; + bool valid; + char *str; /* JSON_STRING:指向 buf 内字符串(不一定 NUL 终止!) */ + size_t str_len; /* JSON_STRING:字符串长度(配合 str 使用,避免就地写 NUL) */ + double num; /* JSON_NUMBER / JSON_BOOL */ + char *obj_start; /* JSON_OBJECT:'{' 后的内容区 */ + size_t obj_len; +} JsonView; + +/** + * @brief 解析 JSON 字符串为根对象视图(就地扫描,可能写入 NUL 终止符)。 + * @param json 可写 NUL 终止 JSON 字符串(须为顶层 { ... } 扁平对象) + * @return type=JSON_OBJECT 且 valid=true,或 valid=false 解析失败 + */ +static inline JsonView jsonParse(char *json) +{ + JsonView v = {}; + if (!json) return v; + + char *p = json; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p != '{') return v; + + p++; + char *content = p; + int depth = 1; + while (*p) { + if (*p == '"') { + p++; + while (*p && *p != '"') { + if (*p == '\\' && p[1]) p++; + p++; + } + if (*p) p++; + } else if (*p == '{') { + depth++; p++; + } else if (*p == '}') { + depth--; + if (depth == 0) break; + p++; + } else { + p++; + } + } + if (depth != 0) return v; + + v.type = JSON_OBJECT; + v.valid = true; + v.obj_start = content; + v.obj_len = (size_t)(p - content); + return v; +} + +/** + * @brief 在扁平 JSON 对象中查找指定 key 的值。 + * @return valid=true 表示找到;字符串值经就地 NUL 终止后通过 .str 返回 + */ +static inline JsonView jsonGet(const JsonView *obj, const char *key) +{ + JsonView result = {}; + if (!obj || !obj->valid || obj->type != JSON_OBJECT || !key) return result; + + char *p = obj->obj_start; + char *end = obj->obj_start + obj->obj_len; + + while (p < end) { + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++; + if (p >= end) break; + + /* "key" */ + if (*p != '"') break; + p++; + char *key_start = p; + while (p < end && *p != '"') p++; + if (p >= end) break; + size_t klen = (size_t)(p - key_start); + p++; + + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + if (p >= end || *p != ':') break; + p++; + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + if (p >= end) break; + + bool match = (strlen(key) == klen && strncmp(key_start, key, klen) == 0); + + if (*p == '"') { + p++; + char *val_start = p; + while (p < end && *p != '"') { + if (*p == '\\' && p + 1 < end) p++; + p++; + } + if (p >= end) break; + if (match) { + /* 不就地写 NUL(会破坏后续 jsonGet 扫描);用 str_len 返回长度 */ + result.type = JSON_STRING; + result.valid = true; + result.str = val_start; + result.str_len = (size_t)(p - val_start); + return result; + } + p++; + } else if (*p == 't' || *p == 'f') { + if (match) { + result.type = JSON_BOOL; + result.valid = true; + result.num = (*p == 't') ? 1.0 : 0.0; + return result; + } + while (p < end && *p != ',' && *p != '}' && *p != ' ' && *p != '\n' && *p != '\r') p++; + } else if (*p == 'n') { + if (match) { + result.type = JSON_NULL; + result.valid = true; + return result; + } + while (p < end && *p != ',' && *p != '}' && *p != ' ' && *p != '\n' && *p != '\r') p++; + } else { + char *val_start = p; + if (*p == '-') p++; + while (p < end && ((*p >= '0' && *p <= '9') || *p == '.' || *p == 'e' || *p == 'E' || *p == '+' || *p == '-')) p++; + if (match) { + /* 不就地写 NUL;strtod 可在非 NUL 终止串上工作(指定长度)。 + * 但 strtod 需要终止符——用临时方式:记录起止,用 sscanf 或手动解析。 + * 更简方案:buf 本身是 NUL 终止的(jsonParse 要求 NUL 终止输入), + * 而 *p 此处是数字后的分隔符(',' 或 '}' 或空白),不是 NUL。 + * 我们临时保存 *p、写 NUL、解析、恢复 *p。 */ + char saved = *p; + *p = '\0'; + result.type = JSON_NUMBER; + result.valid = true; + result.num = strtod(val_start, nullptr); + *p = saved; /* 恢复,不影响后续 jsonGet 扫描 */ + return result; + } + } + } + return result; +} + +/* ── 便捷谓词(兼容 cJSON 调用风格)── */ + +static inline bool jsonIsString(const JsonView *item) { + return item && item->valid && item->type == JSON_STRING; +} +static inline bool jsonIsNumber(const JsonView *item) { + return item && item->valid && item->type == JSON_NUMBER; +} +static inline bool jsonIsBool(const JsonView *item) { + return item && item->valid && item->type == JSON_BOOL; +} +static inline bool jsonIsTrue(const JsonView *item) { + return item && item->valid && item->type == JSON_BOOL && item->num != 0.0; +} + +/** + * @brief 获取字符串值指针和长度(字符串不一定 NUL 终止,须配合 str_len 使用)。 + * @param item JSON 值视图 + * @param out_len 可选,输出字符串长度 + * @return 字符串指针(指向 buf 内部),或 nullptr + */ +static inline const char *jsonGetStringValue(const JsonView *item, size_t *out_len = nullptr) { + if (item && item->valid && item->type == JSON_STRING) { + if (out_len) *out_len = item->str_len; + return item->str; + } + if (out_len) *out_len = 0; + return nullptr; +} + +/** + * @brief 比较字符串值是否等于指定 C 字符串。 + * 因 jsonGet 不写 NUL 终止符,不能用 strcmp,须用此函数。 + */ +static inline bool jsonStringEquals(const JsonView *item, const char *cstr) { + if (!jsonIsString(item) || !cstr) return false; + size_t clen = strlen(cstr); + return clen == item->str_len && strncmp(item->str, cstr, clen) == 0; +} + +/** + * @brief 将字符串值拷贝到 NUL 终止的输出缓冲(安全截断)。 + * @return 实际拷贝长度(不含 NUL) + */ +static inline size_t jsonGetStringCopy(const JsonView *item, char *out, size_t out_max) { + if (!jsonIsString(item) || !out || out_max == 0) return 0; + size_t n = (item->str_len < out_max - 1) ? item->str_len : out_max - 1; + memcpy(out, item->str, n); + out[n] = '\0'; + return n; +} + +static inline double jsonGetNumberValue(const JsonView *item) { + return (item && item->valid && item->type == JSON_NUMBER) ? item->num : 0.0; +} + +#ifdef __cplusplus +} +#endif + +#endif // JSON_PARSE_H \ No newline at end of file diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index cd5322f..6003945 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -299,7 +299,10 @@ esp_err_t WifiOta::initSta() country.policy = WIFI_COUNTRY_POLICY_MANUAL; wifi_config_t wifi_config = {}; - wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + /* FAST_SCAN:扫到目标 SSID 即停并连接(多 AP 同名时取先扫到的,可能非最强)。 + * 原 ALL_CHANNEL_SCAN 要把 13 信道全扫完才连,OTA 重启后页面恢复要多等 3~8s。 + * esp_restart 不清 nvs.net80211 缓存,驱动可复用上次 AP 信息,恢复更快。 */ + wifi_config.sta.scan_method = WIFI_FAST_SCAN; wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; wifi_config.sta.pmf_cfg.capable = true; @@ -822,6 +825,12 @@ void WifiOta::taskEntry(void *pvParameters) /* 启动日志 burst 已过:环 64→16 槽,释放大约 7.5KB 给业务堆 */ DebugLog::instance().shrinkRingAfterBoot(); + /* ★ 重启完成判定信号(前端 polling.js 匹配 /OTA Relay .*启动/ + tag=MAIN): + * 必须在本函数(httpd 就绪、shrinkRingAfterBoot 之后)打印——main.cpp 的 + * 早期 banner 在环缩容时会被裁掉,前端首轮 /logs 拉不到,只能干等 10s + * 安全阈值才恢复页面。此处是环内最新日志,前端一定能拉到。 */ + DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v%s 启动", FW_VERSION); + // WiFi 状态监控 // 每秒一次循环:每 10 秒检查 RSSI uint32_t tick = 0; diff --git a/main/storage/gd32_flash_store.cpp b/main/storage/gd32_flash_store.cpp index 12e21fa..af9474e 100644 --- a/main/storage/gd32_flash_store.cpp +++ b/main/storage/gd32_flash_store.cpp @@ -14,6 +14,7 @@ #include "mbedtls/private/sha256.h" #include "gd32_flash_store.h" +#include "app_config.h" // OTA_BLOCK_SIZE(flashWriteChunk 静态对齐缓冲容量) #include "monitor/debug_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -147,6 +148,54 @@ esp_err_t Gd32FlashStore::ensureErasedForWrite(uint32_t offset, size_t len) return ESP_OK; } +// ============================================================ +// OTA 期对齐缓冲池(2026-08 内存优化) +// +// flashWriteChunk 在非 4 字节对齐写入时需要临时缓冲读回 padding。 +// 原实现每次 malloc/free,OTA 收流时与非对齐块叠加产生堆抖动。 +// 现改为 OTA begin 时一次性 calloc,end/abort 时 free,消除抖动 +// 又不增加常驻 BSS(非 OTA 期零占用)。 +// ============================================================ +static uint8_t *s_align_buf = nullptr; +static constexpr size_t kAlignBufCap = OTA_BLOCK_SIZE + 8; + +void Gd32FlashStore::acquireAlignBufForOta() +{ + if (!s_align_buf) { + s_align_buf = (uint8_t *)calloc(1, kAlignBufCap); + if (!s_align_buf) { + DEBUG_WARN("OTA", "对齐缓冲分配失败,回退 malloc free=%u", + (unsigned)esp_get_free_heap_size()); + } + } +} + +void Gd32FlashStore::releaseAlignBufForOta() +{ + if (s_align_buf) { + free(s_align_buf); + s_align_buf = nullptr; + } +} + +/** @brief 取对齐缓冲。优先用 OTA 期预分配;未分配或太小则临时 malloc。 */ +static uint8_t *acquireAlignBuf(size_t needed) +{ + if (s_align_buf && needed <= kAlignBufCap) { + return s_align_buf; + } + // OTA 未预分配(begin 失败)或包过大:回退临时 malloc + return (uint8_t *)malloc(needed); +} + +/** @brief 归还对齐缓冲。OTA 池的不释放;临时 malloc 的 free。 */ +static void releaseAlignBuf(uint8_t *buf) +{ + if (buf && buf != s_align_buf) { + free(buf); + } +} + /** @brief 向 gd32_ota 分区写入数据块,自动处理非 4 字节对齐的读写。 */ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len) { @@ -188,9 +237,12 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, offset, len, head_pad); } - uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len); + /* OTA 期按需对齐缓冲(OTA begin 时 calloc,end/abort 时 free): + * 消除每非对齐块的 malloc/free 抖动,又不增加常驻 BSS。 + * 容量 = OTA_BLOCK_SIZE(4KB) + 最多 3 字节 tail pad。 */ + uint8_t *aligned_buf = acquireAlignBuf(aligned_len); if (!aligned_buf) { - DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len); + DEBUG_ERR("OTA", "flashWriteChunk 对齐缓冲不足/未分配: %zu", aligned_len); return ESP_ERR_NO_MEM; } @@ -200,7 +252,7 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, if (err != ESP_OK) { DEBUG_ERR("OTA", "flashWriteChunk读回head失败: offset=%" PRIu32 " head=%zu err=%s", aligned_offset, head_pad, esp_err_to_name(err)); - free(aligned_buf); + releaseAlignBuf(aligned_buf); return err; } } @@ -215,13 +267,13 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, if (err != ESP_OK) { DEBUG_ERR("OTA", "flashWriteChunk读回tail失败: offset=%" PRIu32 " tail=%zu err=%s", aligned_offset + total, tail_pad, esp_err_to_name(err)); - free(aligned_buf); + releaseAlignBuf(aligned_buf); return err; } } esp_err_t err = esp_partition_write(part, aligned_offset, aligned_buf, aligned_len); - free(aligned_buf); + releaseAlignBuf(aligned_buf); return err; } diff --git a/main/storage/gd32_flash_store.h b/main/storage/gd32_flash_store.h index 2646347..f258fec 100644 --- a/main/storage/gd32_flash_store.h +++ b/main/storage/gd32_flash_store.h @@ -145,6 +145,12 @@ public: */ const esp_partition_t* getPartition(); + // ── OTA 期对齐缓冲生命周期 ── + /** @brief GD32 OTA begin 时调用:预分配对齐缓冲(消除 flashWriteChunk 逐块 malloc)。 */ + static void acquireAlignBufForOta(); + /** @brief GD32 OTA end/abort 时调用:释放对齐缓冲(非 OTA 期零占用)。 */ + static void releaseAlignBufForOta(); + private: Gd32FlashStore(); ~Gd32FlashStore() = default; diff --git a/main/transport/canfd_bridge.cpp b/main/transport/canfd_bridge.cpp index 62f8a32..149ea2f 100644 --- a/main/transport/canfd_bridge.cpp +++ b/main/transport/canfd_bridge.cpp @@ -61,11 +61,13 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id, uint8_t resp_type = 0U; size_t resp_len = 0U; esp_err_t ret = ESP_OK; - /* CAN 发送只会收到 ACK(0x80) 或 ERROR(0x82)。若读到 WAIT(0x81),必是上一轮电机 OTA + /* 0xC0 发送只会收到 ACK(0x80) 或 ERROR(0x82)。若读到 WAIT(0x81),必是上一轮电机 OTA * 喂数(0xD1→WAIT)残留在 SPI 响应管道里的陈旧帧(0xC0 命令本身永不产生 WAIT)。 * 陈旧 WAIT 会被下方误判为 ERROR → resp_len=0 → gd32_err=0xFF → 无声拒绝发送( - * 表现为“失败一次就再也发不出去”)。此处最多重试几次排空陈旧帧, - * 直到读到本命令真正的 ACK/ERROR。 */ + * 表现为“失败一次就再也发不出去”)。此处最多重试 4 次排空陈旧帧, + * 直到读到本命令真正的 ACK/ERROR。 + * SPI 传输失败/无响应的重发已由 SpiMaster::otaSendAndPoll 统一处理 + * (同 TX_SEQ,最多 3 次尝试),此处不再各自重试;GD32 拒绝(ERROR)同样不重试。 */ for (uint8_t attempt = 0U; attempt < 4U; ++attempt) { resp_type = 0U; resp_len = 0U; @@ -73,8 +75,8 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id, kSpiCanFrameType, tx_buf, tx_len, &resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms); - if (ret != ESP_OK || resp_type != SPI_OTA_RSP_WAIT) { - break; /* SPI 失败或已拿到真正响应(ACK/ERROR) */ + if (resp_type != SPI_OTA_RSP_WAIT) { + break; /* 已拿到真正响应(ACK/ERROR);SPI 失败(ret!=OK)也退出交上层处理 */ } vTaskDelay(pdMS_TO_TICKS(3)); /* 让 GD32 更新响应,再读一次排空陈旧 WAIT */ } @@ -241,8 +243,10 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len, const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX) ? e.dlc : static_cast(CANFD_ID_SNAPSHOT_DATA_MAX); - const uint16_t entrySize = static_cast(8U + dataLen); - if (offset + entrySize > len) { + // 线协议条目实际大小按原始 dlc 计算(CANFD 帧可能 64B),offset 必须按 + // 实际条目推进,否则后续条目错位。data 拷贝按 dataLen 截断。 + const uint16_t wireEntrySize = static_cast(8U + e.dlc); + if (offset + wireEntrySize > len) { break; } @@ -251,7 +255,7 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len, e.data[d] = 0U; } - offset += entrySize; + offset += wireEntrySize; (*entryIdx)++; } @@ -264,6 +268,55 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len, return 0; } +/* ── 0xC1 请求/响应查询(2026-08 架构改造:替代被动推送缓存)── */ + +int canFdQueryFeedback(uint8_t channel, uint8_t page, + can_feedback_result_t *out, uint8_t *entryIdx) +{ + if (out == nullptr || entryIdx == nullptr) { + return -1; + } + if (channel >= CAN_CHANNEL_MAX || page > 1U) { + return -1; + } + + uint8_t req[2] = { channel, page }; + uint8_t resp_type = 0; + /* SPI v2 payload 上限 236B(256 帧 - 20 开销),与 GD32 kFeedbackPayloadLen 一致 */ + uint8_t resp_buf[SPI_OTA_FRAME_MAX]; + size_t resp_len = 0; + + esp_err_t ret = SpiMaster::instance().transact( + 0xC1, req, sizeof(req), + &resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U); + + if (ret != ESP_OK) { + DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s", + (unsigned)channel, (unsigned)page, esp_err_to_name(ret)); + return -1; + } + if (resp_type == SPI_OTA_RSP_ERROR) { + uint8_t gd32_err = (resp_len > 0) ? resp_buf[0] : 0; + DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u GD32拒绝 err=0x%02X", + (unsigned)channel, (unsigned)page, (unsigned)gd32_err); + return -2; + } + if (resp_type != SPI_OTA_RSP_ACK || resp_len < 1U) { + DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u 非ACK type=0x%02X len=%zu", + (unsigned)channel, (unsigned)page, (unsigned)resp_type, resp_len); + return -1; + } + + /* 解析复用现有 canFdParseFeedbackPage(payload 格式与推送一致) */ + int parse_rc = canFdParseFeedbackPage(resp_buf, resp_len, out, entryIdx); + if (parse_rc != 0) { + DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u 解析失败 rc=%d len=%zu", + (unsigned)channel, (unsigned)page, parse_rc, resp_len); + return -1; + } + return 0; +} + /** * @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。 * @return 0 成功;-1 参数非法或 SPI/应答失败。 diff --git a/main/transport/canfd_bridge.h b/main/transport/canfd_bridge.h index b45fafb..2b435d4 100644 --- a/main/transport/canfd_bridge.h +++ b/main/transport/canfd_bridge.h @@ -57,7 +57,7 @@ int canFdSend(can_channel_t channel, uint32_t ext_id, // 不重复 ID 快照表(SPI 0xC3 协议配套) // ============================================================ #define CANFD_ID_SNAPSHOT_MAX 16 ///< 每通道最大不重复 ID 条目数 -#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数 +#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数(CANFD 最大 64B,保持完整不截断) /** * @brief 单个不重复 ID 的最新帧快照条目。 @@ -131,6 +131,22 @@ typedef struct { int canFdParseFeedbackPage(const uint8_t *payload, size_t len, can_feedback_result_t *out, uint8_t *entryIdx); +/** + * @brief 主动向 GD32 查询一页 CAN 反馈(SPI 0xC1 请求/响应)。 + * + * 2026-08 架构改造:ESP32 不再被动缓存 GD32 推送,改为前端请求时按需查询。 + * GD32 handleFeedback 接收 [channel, page] 返回 ACK + 页数据(与推送 payload 格式一致)。 + * 查询成功后调 canFdParseFeedbackPage 解析,复用现有累积逻辑。 + * + * @param channel 目标通道 0/1/2。 + * @param page 页号 0 或 1(page 0 重置累积 + 填统计,page 1 追加 entries)。 + * @param out 累积结果(跨 page 0/1 累积,调用方按通道维护)。 + * @param entryIdx 当前已填充条目数(page 0 时被重置为 0)。 + * @return 0 成功;-1 参数/SPI 失败;-2 GD32 返回 ERROR。 + */ +int canFdQueryFeedback(uint8_t channel, uint8_t page, + can_feedback_result_t *out, uint8_t *entryIdx); + /** * @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。 * @param mode 目标模式(0=全收,1=按 ID 过滤,2=CAN0 桥接到 1/2)。 diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index fd25337..0ea8757 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -21,6 +21,7 @@ #include "esp_log.h" #include "esp_random.h" // esp_random() for HELLO session_id #include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM +#include "esp_timer.h" // esp_timer_get_time():worker job 执行耗时采样 #include "spi_master.h" #include "spi_link_v2.h" #include "app_config.h" @@ -67,8 +68,27 @@ static size_t spiRespReadBudget(void) return SPI_APP_TX_BUF_SIZE; } +/** + * @brief 已知"短响应"的主动请求类型:应答帧 ≤ ~70B(实测 0xA1 PING ~60-70B、 + * DATA/REQ/FINISH ACK ~30-40B、0xB5/0xC0/0xC2/0xD1 ACK 均短)。 + * 对这些类型的主动 poll 可把读预算从 256 收紧到 96B,省 ~160B 0xFF 填充时钟 + * (@13.3MHz ≈ 96µs/帧)。排除 0xC3(ID 快照响应可能很长)与被动 0xC1 事件读。 + */ +static bool isShortRespType(uint8_t type) +{ + switch (type) { + case 0xA1U: case 0x02U: case 0x01U: case 0xF7U: + case 0xB0U: case 0xC0U: case 0xC2U: case 0xD1U: + return true; + default: + return false; + } +} +/** 短响应读预算(覆盖最长 ~70B 应答 + 余量)。 */ +static constexpr size_t kShortRespReadBudget = 96U; + /** APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */ -static bool isAppOnlySpiType(uint8_t type) +bool SpiMaster::isAppOnlySpiType(uint8_t type) { switch (type) { case 0xC1U: /* CAN feedback */ @@ -85,6 +105,34 @@ static bool isAppOnlySpiType(uint8_t type) } } +/* ── 去重辅助实现 ── */ + +void SpiMaster::copyFoundFrame(uint8_t *resp_type, uint8_t found_type, + uint8_t *resp_payload, size_t resp_max, + const uint8_t *found_payload, size_t found_len, + size_t *resp_len) +{ + if (resp_type) *resp_type = found_type; + if (resp_payload && resp_max > 0) { + size_t copy_len = (found_len < resp_max) ? found_len : resp_max; + if (found_payload && copy_len > 0) { + memcpy(resp_payload, found_payload, copy_len); + } else if (copy_len > 0) { + memset(resp_payload, 0, copy_len); + } + } + if (resp_len) *resp_len = found_len; +} + +void SpiMaster::correctRoleFromType(uint8_t found_type, const char *src) +{ + if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) { + DEBUG_INFO("SPI", "role %u→APP (saw type=0x%02X, %s)", + (unsigned)m_gd32Role, found_type, src); + m_gd32Role = V2_ROLE_APP; + } +} + /** @brief ISR-safe GPIO 电平读取(不调用 Flash 中的 gpio_get_level)。 * 用于 SPIready 边沿中断,避免在 cache 被禁用期间访问 Flash 代码。 */ @@ -189,18 +237,52 @@ bool SpiMaster::popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t } /** - * @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理。 - * @param suppress true=OTA 等关键路径禁止偷读 PB6;false=恢复 + * @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理(引用计数)。 + * @param suppress true=+1(OTA 等关键路径禁止偷读 PB6);false=-1(钳位到 0)。 + * + * GD32 中继(forwardFirmware)与电机 OTA feedTask 都可能持有抑制;任一 + * 持有者退出时若直接清位,会把另一方的保护误清(电机 OTA 收尾曾把进行中 + * 的 GD32 中继抑制清掉 → worker 偷读 OTA ACK/WAIT → poll 空等 6s)。 */ void SpiMaster::setSuppressRingService(bool suppress) { - m_suppressRingService = suppress; + taskENTER_CRITICAL(&s_spiJobMux); + uint32_t refs = m_ringSuppressRefs; if (suppress) { + ++refs; + } else if (refs > 0U) { + --refs; + } + m_ringSuppressRefs = refs; + const bool now_suppressed = (refs > 0U); + taskEXIT_CRITICAL(&s_spiJobMux); + + m_suppressRingService = now_suppressed; + if (now_suppressed) { /* 丢弃已锁存的按铃标志,避免解除后立刻误读残留边沿 */ m_gd32RequestPending = false; } } +/** + * @brief ESP32 自我 OTA 期间暂停/恢复全部 SPI 活动。 + * + * 暂停:worker 空转不读 SPI(GD32 按铃丢弃),submitJob 快速拒绝 + * (业务调用立即返回 ESP_ERR_INVALID_STATE,不排队不阻塞)。 + * 恢复:唤醒 worker 恢复正常。幂等,可多次调用。 + */ +void SpiMaster::setSpiPaused(bool paused) +{ + m_spiPaused = paused; + if (paused) { + /* 丢弃已锁存的按铃标志:暂停期间不读 GD32,解除后勿误读残留边沿 */ + m_gd32RequestPending = false; + } + if (m_workerTask != nullptr) { + xTaskNotify(m_workerTask, 0x10U, eSetBits); /* 唤醒 worker 处理状态变化 */ + } +} + /** * @brief 业务侧确认本端 TX 序号已被对端接受,推进 m_txSeq。 * @note 在收到明确业务 ACK 后调用,避免重发与序号失配。 @@ -278,11 +360,7 @@ esp_err_t SpiMaster::serviceRingOnce() { LockGuard lock(m_mutex); /* APP 独有事件 → 纠正粘滞的 boot role(OTA 后 HELLO 未刷新时常见) */ - if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) { - DEBUG_INFO("SPI", "role %u→APP (saw type=0x%02X)", - (unsigned)m_gd32Role, found_type); - m_gd32Role = V2_ROLE_APP; - } + correctRoleFromType(found_type, "ring"); (void)pushEvent(found_type, found_payload, found_len); } return ESP_OK; @@ -428,6 +506,10 @@ void SpiMaster::finishPoolJob(Job *job) */ esp_err_t SpiMaster::submitJob(Job &job) { + /* ESP32 自我 OTA 暂停:快速拒绝,不排队不等待(worker 空转中)。 */ + if (m_spiPaused) { + return ESP_ERR_INVALID_STATE; + } if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) { executeJob(job); return job.result; @@ -529,6 +611,15 @@ void SpiMaster::workerLoop() DEBUG_INFO("SPI", "SPI worker started, handle=%p", (void *)m_workerTask); for (;;) { + /* ESP32 自我 OTA 暂停:空转不读 SPI(flash 写期间 cache 失效会拖慢 + * flash 驻留代码,且此时 GD32 数据无意义)。恢复由 setSpiPaused(false) + * 通知唤醒。 */ + if (m_spiPaused) { + m_gd32RequestPending = false; + uint32_t bits = 0U; + (void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(50)); + continue; + } /* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。 * 禁止见 DATA_READY 低就 serviceRing:命令 HELLO_ACK/OTA 响应也会拉低 * PB6,会被偷走当事件 → 业务 poll 全 0xFF,OTA/ENTER_BOOT 全挂。 @@ -541,7 +632,16 @@ void SpiMaster::workerLoop() Job *job = nullptr; if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) { + /* 计时 worker 实际执行段(取帧→发→读响应→完成): + * 测速页 RTT − 本值 = 提交/调度/唤醒开销,用于区分 + * "ESP32 侧任务抢占" vs "GD32 应答慢" 的抖动归属。 */ + const int64_t w0 = esp_timer_get_time(); + m_lastProfile.t_w0 = (uint64_t)w0; executeJob(*job); + const int64_t w1 = esp_timer_get_time(); + m_lastProfile.t_w1 = (uint64_t)w1; + const int64_t wus = w1 - w0; + m_lastWorkerExecUs = (wus > 0) ? (uint32_t)wus : 0U; finishPoolJob(job); continue; } @@ -698,7 +798,9 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len, t.tx_buffer = tx_ptr; t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL; - /* SPI 事务等待使用有界超时 */ + /* 中断模式(queue+result 信号量):2026-08-02 实测 polling 模式在 C3 单核上 + * 整体更慢——传输段虽略快(-14~-17µs),但 worker 忙等占死 CPU,提交/调度 + * 13→140µs、唤醒 18→76µs、2-5ms 尾巴 23→129 帧,RTT 658→822µs。回退。 */ const uint32_t effective_ms = (timeout_ms == 0U) ? 500U : (timeout_ms < 50U ? 50U : timeout_ms); const TickType_t queue_wait = pdMS_TO_TICKS(effective_ms); @@ -755,7 +857,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len, * 时钟预算见 spiRespReadBudget():BOOT=128,APP=256。 */ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len, - uint32_t timeout_ms) + uint32_t timeout_ms, size_t budget_hint) { if (out_frame == nullptr || out_len == nullptr || out_max < V2_HEADER_LEN) { return ESP_ERR_INVALID_ARG; @@ -767,6 +869,9 @@ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_ *out_len = 0U; size_t budget = spiRespReadBudget(); + if (budget_hint > 0U && budget_hint < budget) { + budget = budget_hint; /* 主动 poll 按请求类型收紧,省 0xFF 填充时钟 */ + } if (budget > out_max) { budget = out_max; } @@ -1069,7 +1174,11 @@ esp_err_t SpiMaster::init() } } if (m_workerTask == nullptr) { - BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 6, &m_workerTask); + /* 优先级 8(2026-08-02 测速实验):原 6 会被 ota_feed(7) 抢占, + * SPI 完成 ISR 唤醒 worker 后还要等 feed 让出,拖长 RTT 唤醒链。 + * 8 仍低于 health_mon(21)/ota_relay(23);worker 阻塞循环为主, + * 仅 job 执行时运行,不会饿死 ota_feed/httpd。 */ + BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 8, &m_workerTask); if (ok != pdPASS || m_workerTask == nullptr) { DEBUG_ERR("SPI", "SPI worker task create failed"); m_workerTask = nullptr; @@ -1124,6 +1233,8 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) { return ESP_ERR_INVALID_ARG; } + /* 记录最近一次主动请求类型:poll 读预算按它收紧(isShortRespType)。 */ + m_lastReqType = type; DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u", type, (unsigned)tx_len); @@ -1194,17 +1305,8 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s /* 与 Git 稳定路径一致:step1 陈旧帧原样解析即可(otaSendAndPoll 传 nullptr 丢弃)。 * 勿在此处过滤 LINK/EVENT,否则改变 delayed-by-one 语义。 */ if (otaFindFrame(rx_raw, xfer_len, &found_type, &found_payload, &found_len, &frame_offset)) { - if (resp_type) *resp_type = found_type; - if (resp_payload && resp_max > 0) { - size_t copy_len = (found_len < resp_max) ? found_len : resp_max; - if (found_payload && copy_len > 0) { - memcpy(resp_payload, found_payload, copy_len); - } - } - if (resp_len) *resp_len = found_len; - if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) { - m_gd32Role = V2_ROLE_APP; - } + copyFoundFrame(resp_type, found_type, resp_payload, resp_max, found_payload, found_len, resp_len); + correctRoleFromType(found_type, "send"); /* expect_stale(两步法 step1)读到的是延迟一拍陈旧帧,序号由 poll 成功时推进。 * 此处若自增会导致 job 间隙/超时后 resend 使用错误 TX_SEQ。 */ if (!expect_stale) { @@ -1286,7 +1388,12 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p ? ((timeout_ms >= 80U) ? 40U : timeout_ms) : 15U; const uint32_t wait_ms = (ready_slice < remain_ms) ? ready_slice : remain_ms; + /* 分段剖析:等 READY 段 = GD32 收到帧→拉低 PB6 的处理时间 */ + m_lastProfile.t_wait0 = (uint64_t)esp_timer_get_time(); const bool ready = (waitSpiDataReady(wait_ms) == ESP_OK); + if (ready) { + m_lastProfile.t_wait1 = (uint64_t)esp_timer_get_time(); + } now = xTaskGetTickCount(); remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U; @@ -1303,7 +1410,12 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p uint8_t rx_raw[SPI_OTA_FRAME_MAX]; size_t rx_len = 0U; - esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout); + /* 读预算按最近请求类型收紧(短响应类型 96B,其余按角色默认) */ + const size_t budget_hint = isShortRespType(m_lastReqType) ? kShortRespReadBudget : 0U; + esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout, budget_hint); + if (ret == ESP_OK) { + m_lastProfile.t_read1 = (uint64_t)esp_timer_get_time(); + } did_spi_read = true; if (ret != ESP_OK) { if (ret == ESP_ERR_INVALID_RESPONSE) { @@ -1347,11 +1459,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p } if ((found_flags & V2_FLAG_EVENT) != 0U) { - if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) { - DEBUG_INFO("SPI", "role %u→APP (poll saw type=0x%02X)", - (unsigned)m_gd32Role, found_type); - m_gd32Role = V2_ROLE_APP; - } + correctRoleFromType(found_type, "poll"); DEBUG_DBG("SPI", "poll skip EVENT type=0x%02X", found_type); vTaskDelay(pdMS_TO_TICKS(1)); continue; @@ -1365,16 +1473,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p continue; } - if (resp_type) *resp_type = found_type; - if (resp_payload && resp_max > 0) { - size_t copy_len = (found_len < resp_max) ? found_len : resp_max; - if (found_payload && copy_len > 0) { - memcpy(resp_payload, found_payload, copy_len); - } else if (copy_len > 0) { - memset(resp_payload, 0, copy_len); - } - } - if (resp_len) *resp_len = found_len; + copyFoundFrame(resp_type, found_type, resp_payload, resp_max, found_payload, found_len, resp_len); /* WAIT/ACK/ERROR:只要 ack_seq 匹配,GD32 已消费本 TX_SEQ,必须推进。 * ★ 电机 OTA:feedData 缓冲满回 WAIT 时若不清 seq,ESP32 同 seq 重发 @@ -1429,7 +1528,11 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo job.out_payload = resp_payload; job.out_max = resp_max; job.out_len = resp_len; - return submitJob(job); + /* 分段剖析:记录调用方侧两个时间戳(worker 侧在 workerLoop/otaSendAndPoll 内写) */ + m_lastProfile.t_submit0 = (uint64_t)esp_timer_get_time(); + const esp_err_t p_ret = submitJob(job); + m_lastProfile.t_submit1 = (uint64_t)esp_timer_get_time(); + return p_ret; } return otaSendAndPoll(type, payload, payload_len, resp_type, resp_payload, resp_max, resp_len, timeout_ms); @@ -1512,27 +1615,58 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t poll_timeout = 1U; } - esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len, - nullptr, nullptr, 0U, nullptr, send_timeout, - /*expect_stale=*/true); - if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) { - return send_ret; - } + /* 统一重发机制(嵌入 worker,所有业务共用,调用方无需各自重试): + * 仅当"没读到任何有效响应"时同 TX_SEQ 重发(失败路径 seq 不推进): + * - 发送级失败(transferClocked 错误/credit 拒绝):帧大概率未送达 GD32; + * - poll 超时(无响应):帧可能未送达或响应丢失,重发靠 GD32 DUP-SEQ 去重兜底。 + * 读到任何帧(WAIT/ACK/ERROR/陈旧)即成功返回,不重发: + * - 保证 GD32 明确拒绝(ERROR 0x82)天然不重试(如 CAN 负载高/BUS_OFF); + * - OTA relay 的 WAIT 语义、旧帧排空语义均不受影响。 */ + constexpr uint32_t kTxRetryMax = 3U; /* 总尝试次数(1 次首发 + 2 次重发) */ + for (uint32_t attempt = 0U; attempt < kTxRetryMax; ++attempt) { + m_lastProfile.t_send0 = (uint64_t)esp_timer_get_time(); + esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len, + nullptr, nullptr, 0U, nullptr, send_timeout, + /*expect_stale=*/true); + m_lastProfile.t_send1 = (uint64_t)esp_timer_get_time(); + if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) { + if (attempt + 1U < kTxRetryMax) { + DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s", + (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, + type, esp_err_to_name(send_ret)); + vTaskDelay(pdMS_TO_TICKS(3)); + continue; + } + return send_ret; + } - /* CS 释放后的静默期分型处理: - * - 0xD1(电机 OTA 数据)/ 0xA1(APP PING):GD32 APP 在 onCsRelease ISR - * 内 µs 级装好响应并拉低 PB6 → 零盲等,waitSpiDataReady 由下降沿 ISR - * 精确唤醒,旧 1ms 固定延时压制喂速~47KB/s,现由事件驱动提速。 - * - 其他类型(GD32 relay/BOOT AES/HELLO 等):保留 5ms——BOOT 在主循环 - * 处理 CHALLENGE/flash,poll 前 2 轮盲读 CS 过早打进去会打断其 - * EXTI/DMA 时序(实测去掉后 CHALLENGE 超时)。 */ - if (type != 0xD1U && type != 0xA1U) { - vTaskDelay(pdMS_TO_TICKS(5)); - } + /* CS 释放后的静默期分型处理: + * - 0xD1(电机 OTA 数据)/ 0xA1(APP PING):GD32 APP 在 onCsRelease ISR + * 内 µs 级装好响应并拉低 PB6 → 零盲等,waitSpiDataReady 由下降沿 ISR + * 精确唤醒,旧 1ms 固定延时压制喂速~47KB/s,现由事件驱动提速。 + * - 其他类型(GD32 relay/BOOT AES/HELLO 等):保留 5ms——BOOT 在主循环 + * 处理 CHALLENGE/flash,poll 前 2 轮盲读 CS 过早打进去会打断其 + * EXTI/DMA 时序(实测去掉后 CHALLENGE 超时)。 */ + if (type != 0xD1U && type != 0xA1U) { + vTaskDelay(pdMS_TO_TICKS(5)); + } - /* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */ - return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout, - /*expected_type=*/0xFFU); + /* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */ + esp_err_t poll_ret = otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, + poll_timeout, /*expected_type=*/0xFFU); + if (poll_ret == ESP_OK) { + return ESP_OK; /* 已读到有效响应帧(ACK/ERROR/陈旧) */ + } + if (attempt + 1U < kTxRetryMax) { + DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s", + (unsigned)(attempt + 1U), (unsigned)kTxRetryMax, + type, esp_err_to_name(poll_ret)); + vTaskDelay(pdMS_TO_TICKS(3)); + continue; + } + return poll_ret; + } + return ESP_ERR_TIMEOUT; /* 不可达:循环必有返回 */ } /** diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 5b02bd3..07f1459 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -100,6 +100,31 @@ public: /** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */ TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; } + /** @brief 最近一次 worker job 实际执行耗时(µs,worker 内计时)。 + * 测速页用 RTT − 本值拆出"提交/调度/唤醒"开销,定位抖动归属。 */ + uint32_t lastWorkerExecUs() const { return m_lastWorkerExecUs; } + + /** + * @brief 单次 transact 的分段耗时剖析(测速页诊断用)。 + * 5 段:提交/调度(t_submit0→t_w0)、发送帧(t_send0→t_send1)、 + * 等应答(t_wait0→t_wait1)、读响应(t_wait1→t_read1)、唤醒(t_w1→t_submit1)。 + * 写入顺序由 done 信号量保证 happens-before(worker 先写全再 give,调用方 take 后读)。 + */ + struct SpiProfile { + uint64_t t_submit0 = 0; ///< 调用方 transact 入口 + uint64_t t_w0 = 0; ///< worker executeJob 开始 + uint64_t t_send0 = 0; ///< 发送帧开始 + uint64_t t_send1 = 0; ///< 发送帧结束 + uint64_t t_wait0 = 0; ///< 进入 poll(等 READY) + uint64_t t_wait1 = 0; ///< READY 就绪(wait 段结束) + uint64_t t_read1 = 0; ///< 响应帧读取完成 + uint64_t t_w1 = 0; ///< worker executeJob 结束 + uint64_t t_submit1 = 0; ///< 调用方 submitJob 返回 + }; + + /** @brief 最近一次 transact 的剖析快照(仅测速/诊断用)。 */ + SpiProfile lastProfile() const { return m_lastProfile; } + /** @brief SPI 任务队列当前待处理 job 数(固定 Job pool 槽,无堆分配)。 * 供 /system/heap_low_log 堆低水位快照采样,定位"低谷是不是 SPI 队列堆积引起"。 */ UBaseType_t getPendingJobCount() const { return m_jobQueue ? uxQueueMessagesWaiting(m_jobQueue) : 0; } @@ -107,9 +132,18 @@ public: /** * @brief OTA 中继期间禁止 worker 将 DATA_READY 当 EVENT 读取。 * 中继开始设 true,结束/复位设 false。 + * 实现为引用计数:GD32 中继与电机 OTA feed 可并发持有,任一持有者 + * 退出都不会提前解除抑制(避免电机 OTA 收尾把 GD32 中继的抑制误清)。 */ void setSuppressRingService(bool suppress); + /** + * @brief ESP32 自我 OTA 期间暂停全部 SPI 活动(GD32 按铃读 + 业务 transact)。 + * 暂停后 worker 空转不读 SPI,submitJob 快速返回 ESP_ERR_INVALID_STATE + * (业务立即失败,不阻塞排队);恢复时唤醒 worker 恢复正常。幂等。 + */ + void setSpiPaused(bool paused); + /** * @brief 业务层确认当前请求已成功时推进/对齐 m_txSeq。 */ @@ -282,12 +316,22 @@ private: SpiLinkState m_linkState; /** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,仅 SPI worker 消费。 */ volatile bool m_gd32RequestPending = false; + /** ring service 抑制引用计数(setSuppressRingService 内部使用,临界区保护)。 */ + uint32_t m_ringSuppressRefs = 0; /** OTA 中继时为 true:worker 不 serviceRing,避免偷读 OTA ACK/WAIT。 */ volatile bool m_suppressRingService = false; + /** ESP32 自我 OTA 期间为 true:worker 空转、submitJob 快速拒绝。 */ + volatile bool m_spiPaused = false; + /** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */ + volatile uint8_t m_lastReqType = 0xFFU; /** SPI worker 任务句柄(ISR 唯一通知目标)。 */ TaskHandle_t m_spiTaskHandle = NULL; TaskHandle_t m_workerTask = NULL; QueueHandle_t m_jobQueue = NULL; + /** 最近一次 job 执行耗时(µs,worker 写入、业务只读;单核 32 位读原子)。 */ + uint32_t m_lastWorkerExecUs = 0; + /** 最近一次 transact 分段剖析(worker 与调用方交叉写入,done 信号量同步)。 */ + SpiProfile m_lastProfile; /** 事件计数信号量(worker push / takeEvent pop)。 */ SemaphoreHandle_t m_eventSem = NULL; @@ -405,6 +449,19 @@ private: /** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */ static void IRAM_ATTR onReadyEdgeISR(void *arg); + /** @brief APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */ + static bool isAppOnlySpiType(uint8_t type); + + // ── 去重辅助 ── + /** @brief 把 otaFindFrame 找到的帧拷贝到调用方输出缓冲(安全截断)。 */ + static void copyFoundFrame(uint8_t *resp_type, uint8_t found_type, + uint8_t *resp_payload, size_t resp_max, + const uint8_t *found_payload, size_t found_len, + size_t *resp_len); + + /** @brief 见到 APP 独有 TYPE 时纠正粘滞的 boot role(3 处调用合并)。 */ + void correctRoleFromType(uint8_t found_type, const char *src); + // 内部 SPI 传输 /** * @brief 执行底层 SPI 时钟同步传输。 @@ -430,13 +487,15 @@ private: /** * @brief 读一帧响应:单 CS 固定时钟预算(BOOT=128 / APP=256),再 v2_parse 截真实长度。 * 调用者须已持锁。不用两段 KEEP_ACTIVE(Boot 中途断 CS 会清 pending → 全 0xFF)。 - * @param out_frame 完整帧输出(含头+payload+CRC) - * @param out_max 输出缓冲容量 - * @param out_len 实际帧长 - * @param timeout_ms 单次 SPI 超时 + * @param out_frame 完整帧输出(含头+payload+CRC) + * @param out_max 输出缓冲容量 + * @param out_len 实际帧长 + * @param timeout_ms 单次 SPI 超时 + * @param budget_hint 读预算提示(0=按角色默认;>0 时取 min(hint, 角色预算), + * 主动 poll 按最近请求类型收紧,省 0xFF 填充时钟) */ esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len, - uint32_t timeout_ms); + uint32_t timeout_ms, size_t budget_hint = 0U); // 无锁内部版本(供已持锁的公共方法调用,避免递归死锁) /** diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 34b865b..8674fd4 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -12,10 +12,10 @@ CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000 # 不要再开 QIO,除非确认 flash 型号与硬件连接均兼容。 CONFIG_ESPTOOLPY_FLASHMODE_DIO=y -# DCU SPI clock -- 8 MHz(2026-07-31 提速:事件驱动握手后时钟占周期 ~78%, -# 8MHz 实测测速页/relay/PING 零错误后启用;半周期 62.5ns,GD32 从机 MISO -# 输出延迟 + ESP32 GPIO 矩阵输入延迟 ~45ns 仍有余量;≥16MHz 全双工不可行) -CONFIG_DCU_SPI_CLOCK_HZ=8000000 +# DCU SPI clock -- 13.3 MHz(2026-08-02:80MHz/6 整除,比 12M 再省 ~8%; +# 半周期 37.5ns,MISO 输出延迟 + GPIO 矩阵延迟 ~45ns 预算已近上限,需实测; +# ≥16MHz 全双工不可行。写 13333333:若写 13000000 驱动取整 80/7=11.4M 反而更慢) +CONFIG_DCU_SPI_CLOCK_HZ=13333333 # GD32 OTA 包间节奏:不用固定 PACKET_INTERVAL;依赖 SpiMaster CREDIT 背压 # (flash 槽满时 CREDIT=0,等 CREDIT_UPDATE / 帧头 credit 恢复后再发) diff --git a/tools/build_mem.bat b/tools/build_mem.bat new file mode 100644 index 0000000..847f972 --- /dev/null +++ b/tools/build_mem.bat @@ -0,0 +1,23 @@ +@echo off +REM Clear MSYS/MINGW env vars to bypass IDF MSys detection when called from Git Bash +set MSYSTEM= +set MSYSTEM_CHOST= +set MSYSTEM_PREFIX= +set MINGW_CHOST= +set MINGW_PREFIX= +set MINGW_PACKAGE_PREFIX= +set CHERE_INVOKING= +set MSYS2_PATH_TYPE= + +REM Toolchain installed at C:\Espressif (see .vscode/settings.json idf.toolsPath) +set IDF_TOOLS_PATH=C:\Espressif + +call "F:\software\.espressif\v6.0\esp-idf\export.bat" +if errorlevel 1 ( + echo EXPORT_FAILED + exit /b 1 +) + +cd /d "G:\Robot_v2\DCU\ESP32_COMM" +idf.py build +exit /b %errorlevel% \ No newline at end of file diff --git a/tools/js/common/polling.js b/tools/js/common/polling.js index b80e8c7..9a5e8c6 100644 --- a/tools/js/common/polling.js +++ b/tools/js/common/polling.js @@ -216,11 +216,19 @@ export async function pollStatus() { if (window.isEsp32Rebooting || window.otaSessionState === window.OtaSessionState.REBOOTING) { var rbElapsed = Math.round((Date.now() - window.rebootStartTime) / 1000); - // pre-reboot 窗口:后端发响应后约 1s 才 esp_restart,期间设备仍响应轮询, - // 保持"重启中"状态,不清除标志,不推进阶段。 - // ★ 真正的"重启完成"由 pollLogs 收到启动 banner 触发; - // 这里只在已观察到下线(rebootObservedDown)或超 10s 安全阈值时接受恢复。 - if (window.rebootObservedDown || rbElapsed >= 10) { + /* 重启完成的证据(任一即可): + * 1) rebootObservedDown:已观察到设备下线后恢复。 + * 2) freshBoot:设备回报 uptime 很小(<120s,与 resolveEsp32VerifyAmbiguous + * 的 freshBoot 阈值一致)。★ 关键:设备掉线仅 3~5s 时(FAST_SCAN 生效后 + * 常见),在途轮询的 TCP SYN 重传会在设备起来后完成握手、全部成功, + * rebootObservedDown 永远不置位,旧逻辑只能干等 10s 兜底—— + * 实测设备 3s 就绪、页面却 10s 才恢复。uptime 是新固件启动的权威信号。 + * 3) rbElapsed >= 10:安全兜底(兼容 uptime 缺失 / 设备未真重启)。 + * pre-restart 窗口(finalize 响应后约 1s 设备仍在跑旧固件)uptime 很大, + * 不会误判为 freshBoot。 */ + var upS = Number(data && data.uptime_sec); + var freshBoot = Number.isFinite(upS) && upS >= 0 && upS < 120; + if (window.rebootObservedDown || freshBoot || rbElapsed >= 10) { markEsp32RebootComplete(); } } diff --git a/tools/spi_test.html b/tools/spi_test.html index ec6a201..3e71655 100644 --- a/tools/spi_test.html +++ b/tools/spi_test.html @@ -113,15 +113,15 @@ function addHistory(frames, payload, text) { /* 从设备中文文本里提取关键数字做历史行 */ var fail = (text.match(/失败:\s*(\d+)/) || [])[1] || "?"; - var avg = (text.match(/平均\s*([\d.]+)\s*ms/) || [])[1] || "?"; - var min = (text.match(/最快\s*([\d.]+)\s*ms/) || [])[1] || "?"; + var avg = (text.match(/平均\s*([\d.]+)\s*µs/) || [])[1] || "?"; + var min = (text.match(/最快\s*([\d.]+)\s*µs/) || [])[1] || "?"; var tput = (text.match(/等效吞吐:\s*([\d.]+)\s*KB\/s/) || [])[1] || "-"; var d = new Date(); var tr = document.createElement("tr"); var cells = [ pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()), String(frames), String(payload) + "B", fail, - avg + "ms", min + "ms", tput === "-" ? "-" : tput + "KB/s" + avg + "µs", min + "µs", tput === "-" ? "-" : tput + "KB/s" ]; for (var i = 0; i < cells.length; i++) { var td = document.createElement("td");