feat(config): 优化NimBLE配置以节省内存并提升OTA性能

- 启用BLE连接事件长度优化,允许单CE内传输多包,提升OTA吞吐
- 缩减NimBLE mbuf池配置,单连接OTA场景节省约7KB内存
- 禁用未使用的标准GATT服务,仅保留自定义OTA service,节省8-15KB
- 缩减NimBLE传输缓冲区配置以优化内存使用

refactor(debug_log): 重构环形日志缓冲区API以支持逐条弹出

- 新增ringPopOldest方法,支持弹出最旧日志并从缓冲区移除
- 重构ringDrainJson实现,基于新的逐条弹出机制
- 改进日志转义处理逻辑,支持更多特殊字符转义

perf(tasks): 优化各任务栈大小配置以节省内存

- 将ota_relay任务栈从12288字节缩减至6144字节
- 将ble_ota任务栈从8192字节缩减至4096字节
- 将ble_ota数据工作线程栈从8192字节缩减至4096字节
- 通过栈高水位监控验证实际使用情况,确保安全余量

feat(monitor): 添加任务句柄获取接口以支持系统状态监控

- 为HealthMonitor、OtaRelay、BleOta和WifiOta类添加任务句柄获取方法
- 在任务入口处记录当前任务句柄
- 便于系统状态监控和调试信息收集

feat(status): 扩展系统状态API以提供更详细的内存和任务信息

- 增加内部堆和默认堆的详细统计信息
- 添加各关键任务的栈使用情况监控
- 扩展/system/status端点返回更全面的系统资源使用情况
- 包含最大空闲块、最小空闲内存等关键指标
This commit is contained in:
小龙虾
2026-06-20 00:46:04 +08:00
parent 59c729d77a
commit 3fff959a2b
18 changed files with 701 additions and 103 deletions
+2 -2
View File
@@ -95,7 +95,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
OtaRelay::taskEntry,
"ota_relay",
12288,
6144,
NULL,
configMAX_PRIORITIES - 2,
NULL,
@@ -127,7 +127,7 @@ extern "C" void app_main(void)
xRet = xTaskCreatePinnedToCore(
BleOta::taskEntry,
"ble_ota",
8192,
4096,
NULL,
configMAX_PRIORITIES - 3,
NULL,
+4 -4
View File
@@ -4,11 +4,11 @@
// 固件版本
#define FW_VERSION_MAJOR 0
#define FW_VERSION_MINOR 3
#define FW_VERSION_PATCH 28
#define FW_VERSION_STRING "0.3.28"
#define FW_VERSION_PATCH 29
#define FW_VERSION_STRING "0.3.29"
// 编译时间
#define BUILD_TIME "2026-06-19 03:24:28"
#define BUILD_TIME "2026-06-19 23:45:14"
// Git 信息
#define GIT_HASH "7feef70"
#define GIT_HASH "59c729d"
+65 -50
View File
@@ -60,74 +60,89 @@ int DebugLog::ringCount()
}
/**
* @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。
* @param out_len 输出参数,返回 JSON 字符串长度。
* @return 指向 JSON 字符串的指针,需调用者 free。
* @brief 弹出一条最旧日志并从环形缓冲区移除。
* @param out 输出缓冲区。
* @param out_size 输出缓冲区大小。
* @param out_len 输出日志长度。
* @return true 表示成功弹出一条日志,false 表示缓冲区为空或参数无效。
*/
char *DebugLog::ringDrainJson(int *out_len)
bool DebugLog::ringPopOldest(char *out, size_t out_size, int *out_len)
{
if (!m_logRingMutex) {
if (out_len) {
*out_len = 0;
return NULL;
}
if (!m_logRingMutex || !out || out_size == 0) {
return false;
}
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
int count = m_logRingCount;
if (count == 0) {
if (m_logRingCount <= 0) {
xSemaphoreGive(m_logRingMutex);
*out_len = 0;
return NULL;
return false;
}
// 估算 JSON 大小:每条 ~280 字节(转义 + JSON 包装)
size_t buf_size = (size_t)count * 280 + 16;
char *json = (char *)malloc(buf_size);
if (!json) {
xSemaphoreGive(m_logRingMutex);
*out_len = 0;
return NULL;
int idx = (m_logRingHead - m_logRingCount + LOG_RING_SLOTS) % LOG_RING_SLOTS;
int len = m_logRingLens[idx];
if (len < 0) {
len = 0;
}
int pos = 0;
json[pos++] = '[';
// 从最旧的开始读
int start = (count < LOG_RING_SLOTS) ? 0 : m_logRingHead;
for (int i = 0; i < count && pos < (int)buf_size - 200; i++) {
int idx = (start + i) % LOG_RING_SLOTS;
int len = m_logRingLens[idx];
if (len <= 0) continue;
if (i > 0) json[pos++] = ',';
pos += snprintf(json + pos, buf_size - pos, "\"");
for (int j = 0; j < len && pos < (int)buf_size - 10; j++) {
char c = m_logRing[idx][j];
if (c == '"' || c == '\\') {
json[pos++] = '\\';
json[pos++] = c;
} else if (c == '\n') {
json[pos++] = '\\';
json[pos++] = 'n';
} else if (c >= 32) {
json[pos++] = c;
}
}
pos += snprintf(json + pos, buf_size - pos, "\"");
int copy_len = len;
if ((size_t)copy_len >= out_size) {
copy_len = (int)out_size - 1;
}
json[pos++] = ']';
json[pos] = '\0';
if (copy_len > 0) {
memcpy(out, m_logRing[idx], (size_t)copy_len);
}
out[copy_len] = '\0';
if (out_len) {
*out_len = copy_len;
}
// 清空缓冲区
m_logRingHead = 0;
m_logRingCount = 0;
m_logRingLens[idx] = 0;
m_logRingCount--;
xSemaphoreGive(m_logRingMutex);
return true;
}
char* DebugLog::ringDrainJson(int *out_len)
{
if (!out_len) return NULL;
*out_len = 0;
int count = ringCount();
int capacity = count * (LOG_RING_SIZE * 2) + 16;
if (capacity < 16) capacity = 16;
char *buf = (char*)malloc(capacity);
if (!buf) return NULL;
int pos = 0;
buf[pos++] = '[';
bool first = true;
char entry[LOG_RING_SIZE];
int entryLen = 0;
while (ringPopOldest(entry, sizeof(entry), &entryLen) && entryLen > 0) {
if (!first) buf[pos++] = ',';
first = false;
buf[pos++] = '"';
for (int i = 0; i < entryLen && pos < capacity - 4; i++) {
char c = entry[i];
if (c == '"') { buf[pos++] = '\\'; buf[pos++] = '"'; }
else if (c == '\\') { buf[pos++] = '\\'; buf[pos++] = '\\'; }
else if (c == '\n') { buf[pos++] = '\\'; buf[pos++] = 'n'; }
else if (c == '\r') { buf[pos++] = '\\'; buf[pos++] = 'r'; }
else if (c == '\t') { buf[pos++] = '\\'; buf[pos++] = 't'; }
else if ((unsigned char)c >= 0x20) { buf[pos++] = c; }
}
buf[pos++] = '"';
}
buf[pos++] = ']';
buf[pos] = '\0';
*out_len = pos;
return json;
return buf;
}
/**
@@ -350,4 +365,4 @@ void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ..
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
send((const uint8_t *)log_buf, total);
}
}
+3 -2
View File
@@ -72,7 +72,8 @@ public:
/** @brief 获取当前环形日志缓冲区中的日志条数。 */
int ringCount();
/** @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串(需调用者 free)。 */
/** @brief 弹出一条最旧日志并从环形缓冲区移除。 */
bool ringPopOldest(char *out, size_t out_size, int *out_len);
char* ringDrainJson(int *out_len);
/** @brief 当前日志级别(运行时可改)。 */
@@ -107,4 +108,4 @@ private:
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
#endif // DEBUG_LOG_H
#endif // DEBUG_LOG_H
+2 -1
View File
@@ -16,7 +16,6 @@
#include "esp_err.h"
#include "driver/temperature_sensor.h"
#include "esp_heap_caps.h"
#include "health_monitor.h"
#include "debug_log.h"
#include "server/wifi_ota.h"
@@ -167,7 +166,9 @@ void HealthMonitor::tempMonitorCallback(void *arg)
*/
void HealthMonitor::taskEntry(void *pvParameters)
{
(void)pvParameters;
auto& self = instance();
self.m_taskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
+6 -1
View File
@@ -8,6 +8,7 @@
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "driver/temperature_sensor.h"
#include "app_config.h"
@@ -70,6 +71,9 @@ public:
/** @brief 获取最近一次 WiFi RSSI 及其年龄。 */
bool getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms);
/** @brief 获取健康监控任务句柄。 */
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
private:
/** @brief 加锁读取健康状态。 */
health_state_t getState();
@@ -88,6 +92,7 @@ private:
float m_lastTemperature = 0.0f;
int8_t m_lastWifiRssi = -127;
int64_t m_lastWifiRssiMs = 0;
TaskHandle_t m_taskHandle = NULL;
static void tempMonitorCallback(void *arg);
};
};
+7
View File
@@ -41,6 +41,7 @@ gd32_version_t OtaRelay::gd32Version = {};
QueueHandle_t OtaRelay::dispatchQueue = NULL;
QueueHandle_t OtaRelay::resultQueue = NULL;
SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
TaskHandle_t OtaRelay::taskHandle = NULL;
// ============================================================
// 状态表:将每个 relay_state_t 映射到对应的 handler
@@ -134,6 +135,11 @@ void OtaRelay::setGd32Version(const gd32_version_t &ver)
gd32Version = ver;
}
TaskHandle_t OtaRelay::getTaskHandle()
{
return taskHandle;
}
// ============================================================
// 事件分发核心:processEvent()
// ============================================================
@@ -1397,6 +1403,7 @@ ota_result_t OtaRelay::doFinishAndVerify()
void OtaRelay::taskEntry(void *pvParameters)
{
(void)pvParameters;
taskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO("OTA", "OTA 中继任务启动");
initMutex();
+6
View File
@@ -19,6 +19,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "transport/protocol.h"
#include "transport/spi_master.h"
#include "esp_partition.h"
@@ -137,6 +138,10 @@ public:
* @brief 设置GD32固件版本.
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 获取 OTA 中继任务句柄。
*/
static TaskHandle_t getTaskHandle();
/**
* @brief 重置状态机到 IDLE.
@@ -199,6 +204,7 @@ public:
static gd32_version_t gd32Version;
static QueueHandle_t dispatchQueue;
static QueueHandle_t resultQueue;
static TaskHandle_t taskHandle;
// ── 线程安全锁 ──
static SemaphoreHandle_t s_stateMutex;
+3 -1
View File
@@ -1205,6 +1205,7 @@ static void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
static void ble_ota_data_worker_task(void *pvParameters)
{
(void)pvParameters;
BleOta::instance().m_workerTaskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO(TAG, "OTA 数据工作线程已启动");
while (1) {
@@ -1715,6 +1716,7 @@ void BleOta::signalRelay()
void BleOta::taskEntry(void *pvParameters)
{
(void)pvParameters;
BleOta::instance().m_taskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ===");
// 创建 OTA 数据队列和工作线程(在 NimBLE 初始化之前完成)
@@ -1722,7 +1724,7 @@ void BleOta::taskEntry(void *pvParameters)
if (s_otaDataQueue) {
BaseType_t wk_ok = xTaskCreate(ble_ota_data_worker_task,
"ble_ota_worker",
8192, // 6KB→8KB,flash 操作需要足够栈空间
4096, // 峰值仅 ~1.9KB,4KB 留有充足余量
NULL,
10, // 中等优先级,避免被 NimBLE host task 过度抢占
NULL);
+9
View File
@@ -11,6 +11,7 @@
#include <stdint.h>
#include "esp_err.h"
#include "esp_ota_ops.h"
#include "freertos/task.h"
class BleOta {
public:
@@ -47,6 +48,12 @@ public:
*/
void signalRelay();
/** @brief 获取 BLE OTA 守护任务句柄。 */
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
/** @brief 获取 BLE OTA 数据工作线程句柄。 */
TaskHandle_t getWorkerTaskHandle() const { return m_workerTaskHandle; }
// 以下状态为 public,供 C 回调函数访问
bool m_bleReady = false;
bool m_bleConnected = false;
@@ -56,6 +63,8 @@ public:
bool m_esp32RebootPending = false; // 收尾任务去重标志
uint8_t m_esp32LastCmd = 0; // 最后一次数据包命令(用于补发 ACK)
uint16_t m_esp32LastSeq = 0xFFFFU; // 最后一次数据包序号(用于补发 ACK)
TaskHandle_t m_taskHandle = nullptr;
TaskHandle_t m_workerTaskHandle = nullptr;
};
#endif // BLE_OTA_H
+90 -4
View File
@@ -31,6 +31,7 @@
#include "health_monitor.h"
#include "monitor/debug_log.h"
#include "spi_master.h"
#include "ble_ota.h"
#include "cJSON.h"
@@ -1166,6 +1167,46 @@ static esp_err_t spi_send_handler(httpd_req_t *req)
return ESP_OK;
}
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
struct TaskStackMetric {
const char* name;
TaskHandle_t handle;
size_t stack_size_bytes;
};
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
{
if (handle == NULL) return 0;
UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(handle);
return (size_t)highWaterMark;
}
static size_t get_task_stack_peak_used_bytes(TaskHandle_t handle, size_t stack_size_bytes)
{
size_t high_watermark = get_task_stack_high_watermark_bytes(handle);
return (stack_size_bytes >= high_watermark) ? (stack_size_bytes - high_watermark) : 0;
}
/**
* @brief 追加 JSON 内容到响应缓冲区,自动处理 snprintf 截断。
*/
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;
}
/**
* @brief HTTP GET /system/status 处理器
* @param req
@@ -1236,12 +1277,45 @@ static esp_err_t system_status_handler(httpd_req_t *req)
gd32_version_t snapVer2 = {};
OtaRelay::getGd32Version(&snapVer2);
ota_system_state_t sysState = OtaCoordinator::instance().getSystemState();
char resp[1280];
size_t free_heap = esp_get_free_heap_size();
size_t min_free_heap = esp_get_minimum_free_heap_size();
size_t largest_free_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
size_t internal_free_heap = heap_caps_get_free_size(INTERNAL_HEAP_CAPS);
size_t internal_min_free_heap = heap_caps_get_minimum_free_size(INTERNAL_HEAP_CAPS);
size_t internal_largest_free_block = heap_caps_get_largest_free_block(INTERNAL_HEAP_CAPS);
size_t internal_heap_total = heap_caps_get_total_size(INTERNAL_HEAP_CAPS);
size_t default_heap_total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT);
TaskStackMetric task_metrics[] = {
{ "http_server", xTaskGetCurrentTaskHandle(), HTTP_SERVER_STACK_SIZE },
{ "health_mon", HealthMonitor::instance().getTaskHandle(), 4096 },
{ "ota_relay", OtaRelay::getTaskHandle(), 6144 },
{ "wifi_ota", WifiOta::instance().getTaskHandle(), 8192 },
{ "wifi_reconnect", WifiOta::instance().m_reconnectTask, 4096 },
{ "ble_ota", BleOta::instance().getTaskHandle(), 4096 },
{ "ble_ota_worker", BleOta::instance().getWorkerTaskHandle(), 4096 }
};
char resp[4096];
int len = snprintf(resp, sizeof(resp),
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, \"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}}",
"{\"version\": \"%s\", \"build_time\": \"%s\", \"git_hash\": \"%s\", \"health\": %d, "
"\"free_heap\": %lu, \"min_free_heap\": %lu, \"largest_free_block\": %lu, "
"\"internal_free_heap\": %lu, \"internal_min_free_heap\": %lu, \"internal_largest_free_block\": %lu, "
"\"internal_heap_total\": %lu, \"default_heap_total\": %lu, "
"\"temperature\": %.1f, \"uptime_sec\": %lu, "
"\"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, "
"\"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, "
"\"relay_state\": %d, \"relay_seq\": %lu, \"relay_total_packets\": %lu, \"gd32_app_alive\": %s, "
"\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}, "
"\"task_stack\": {",
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)esp_get_minimum_free_heap_size(),
(unsigned long)free_heap,
(unsigned long)min_free_heap,
(unsigned long)largest_free_block,
(unsigned long)internal_free_heap,
(unsigned long)internal_min_free_heap,
(unsigned long)internal_largest_free_block,
(unsigned long)internal_heap_total,
(unsigned long)default_heap_total,
(double)HealthMonitor::instance().getLastTemperature(),
(unsigned long)(esp_timer_get_time() / 1000000),
WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_AP ? "AP" : "STA",
@@ -1262,6 +1336,17 @@ static esp_err_t system_status_handler(httpd_req_t *req)
snapVer2.boot_major, snapVer2.boot_minor,
snapVer2.boot_patch, snapVer2.boot_build);
for (size_t i = 0; i < sizeof(task_metrics) / sizeof(task_metrics[0]); ++i) {
len = append_jsonf(resp, sizeof(resp), len,
"%s\"%s\":{\"stack_size_bytes\":%lu,\"high_watermark_bytes\":%lu,\"peak_used_bytes\":%lu}",
i == 0 ? "" : ",",
task_metrics[i].name,
(unsigned long)task_metrics[i].stack_size_bytes,
(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, sizeof(resp), len, "}}");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, len);
return ESP_OK;
@@ -1648,6 +1733,7 @@ bool WifiOta::earlyInit()
void WifiOta::taskEntry(void *pvParameters)
{
(void)pvParameters;
WifiOta::instance().m_taskHandle = xTaskGetCurrentTaskHandle();
DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
+5
View File
@@ -11,6 +11,7 @@
#include "esp_ota_ops.h"
#include "esp_http_server.h"
#include "esp_event.h"
#include "freertos/task.h"
#include <atomic>
// SHA-256 验证所需类型前向声明
@@ -102,6 +103,9 @@ public:
/** @brief BLE OTA 开始传输时调用,暂停 WiFi RSSI 轮询等射频活动。 */
void setBleOtaActive(bool v) { m_bleOtaActive.store(v); }
/** @brief 获取 WiFi OTA 任务句柄。 */
TaskHandle_t getTaskHandle() const { return m_taskHandle; }
private:
// ── 跨任务共享状态(atomic + m_stateMutex 保护)──
std::atomic<bool> m_staConnected{false};
@@ -110,6 +114,7 @@ private:
std::atomic<bool> m_gd32StorageDone{false};
std::atomic<bool> m_relayTriggered{false};
SemaphoreHandle_t m_stateMutex = NULL;
TaskHandle_t m_taskHandle = nullptr;
/**
* @brief 初始化 SoftAP 模式,供局域网 OTA 使用。