feat(spi): 增强 SPI master:性能剖析与执行耗时跟踪
- 新增 `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 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。
This commit is contained in:
@@ -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 回退路径:释放
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+163
-14
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+400
-340
File diff suppressed because it is too large
Load Diff
@@ -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 <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#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
|
||||
@@ -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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user