refactor(debug): 重构调试日志环形缓冲区管理
- 将日志环形缓冲区从静态数组改为动态堆分配
- 实现启动时大容量缓冲区和运行时小容量缓冲区的切换机制
- 添加日志环满时优先保留错误和警告信息的清理策略
- 优化内存使用,在日志为空时避免不必要的内存分配
- 移除调试日志中的冗余字段显示
feat(api): 更新CAN接口为批量发送模式
- 将 `/can/send` 接口替换为 `/can/send_batch`,支持1到N帧的批量发送
- 更新API文档中的接口描述和使用示例
- 修改请求参数格式为 `{"ch":0,"frames":[{...}]}` 返回格式为 `{"status":"ok","results":[...]}`
- 单帧发送也需要通过send_batch接口实现
feat(monitor): 新增GD32 ADC调试反馈处理功能
- 添加gd32_adc_feedback组件用于解析0xC4类型的SPI反馈
- 实现ADC数据解析包括温度、电压等参数的计算和转换
- 在can_feedback_task中集成ADC反馈分发逻辑
- 添加线程安全的数据获取和年龄检查接口
refactor(config): 优化任务栈大小配置并调整OTA块大小
- 重新评估各任务栈使用情况,优化HTTP、WiFi、SPI等工作任务的栈大小
- 将HTTP服务器栈从8KB调整至7KB,WiFi OTA任务栈从4KB调整至3KB
- OTA块大小从2KB调整回4KB以提升传输吞吐性能
- 重新整理配置文件结构,移动WiFi模式配置位置
This commit is contained in:
+15
-10
@@ -15,6 +15,11 @@ void drainOtaDataQueue()
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ble_ota_get_data_queue_depth()
|
||||
{
|
||||
return s_otaDataQueue ? (uint32_t)uxQueueMessagesWaiting(s_otaDataQueue) : 0;
|
||||
}
|
||||
|
||||
BleOta& BleOta::instance()
|
||||
{
|
||||
static BleOta inst;
|
||||
@@ -145,7 +150,7 @@ void ble_ota_boost_wifi_ps()
|
||||
s_wifi_ps_boosted = true;
|
||||
// 通过协调器通知 WiFi 层
|
||||
OtaCoordinator::instance().notifyBleCoexStart();
|
||||
DEBUG_DBG(TAG, "BLE OTA 数据传输进行中:暂停 WiFi RSSI 轮询(保持 WIFI_PS_NONE,STA 仍可响应 HTTP)");
|
||||
DEBUG_DBG(TAG, "BLE OTA中:暂停RSSI,保持PS_NONE");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,7 +162,7 @@ void ble_ota_restore_wifi_ps()
|
||||
s_wifi_ps_boosted = false;
|
||||
// 通过协调器通知 WiFi 层
|
||||
OtaCoordinator::instance().notifyBleCoexEnd();
|
||||
DEBUG_DBG(TAG, "BLE OTA 数据传输结束:恢复 WiFi RSSI 轮询(GD32 中继仍可能进行中)");
|
||||
DEBUG_DBG(TAG, "BLE OTA数据结束:恢复RSSI轮询");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -286,8 +291,8 @@ esp_err_t BleOta::resumeStack()
|
||||
|
||||
if (!m_stackRunning) {
|
||||
/* 仅当曾 hard-deinit 过才需要完整 serverStart */
|
||||
DEBUG_INFO(TAG, "WiFi OTA 结束:BLE 栈未运行,尝试 serverStart free=%lu",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
DEBUG_DBG(TAG, "WiFi共存:BLE栈未运行,serverStart free=%lu",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
esp_err_t err = serverStart();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "resumeStack/serverStart 失败: %s", esp_err_to_name(err));
|
||||
@@ -297,9 +302,9 @@ esp_err_t BleOta::resumeStack()
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* 软挂起恢复:重新开广播即可 */
|
||||
DEBUG_INFO(TAG, "WiFi OTA 结束:恢复 BLE 广播 free=%lu",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
/* 软挂起恢复:重新开广播即可(非 BLE OTA 会话,仅射频共存;默认 INFO 不刷) */
|
||||
DEBUG_DBG(TAG, "WiFi共存:恢复BLE广播 free=%lu",
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
m_bleReady = true;
|
||||
ble_ota_start_advertising();
|
||||
return ESP_OK;
|
||||
@@ -375,17 +380,17 @@ void BleOta::taskEntry(void *pvParameters)
|
||||
12, // 略提高:尽快排空队列,减少 withoutResponse 连发丢包
|
||||
NULL);
|
||||
if (wk_ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "OTA 数据工作线程创建失败,BLE OTA 数据传输不可用");
|
||||
DEBUG_ERR(TAG, "OTA工作线程创建失败,BLE不可用");
|
||||
vQueueDelete(s_otaDataQueue);
|
||||
s_otaDataQueue = nullptr;
|
||||
}
|
||||
} else {
|
||||
DEBUG_ERR(TAG, "OTA 数据队列创建失败,BLE OTA 数据传输不可用");
|
||||
DEBUG_ERR(TAG, "OTA队列创建失败,BLE不可用");
|
||||
}
|
||||
|
||||
esp_err_t err = BleOta::instance().serverStart();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
|
||||
DEBUG_ERR(TAG, "BLE serverStart失败:%s",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
|
||||
@@ -81,4 +81,12 @@ public:
|
||||
bool m_suspendedForWifiOta = false; ///< 因 WiFi OTA 主动挂起
|
||||
};
|
||||
|
||||
// ── 堆低水位快照辅助 getter(不在 BleOta 类内,file-static 资源访问) ──
|
||||
/** @brief BLE OTA 数据队列当前深度(每个 item 持有 malloc 的 data 缓冲)。
|
||||
* 供 /system/heap_low_log 采样。非 OTA 期返回 0。 */
|
||||
uint32_t ble_ota_get_data_queue_depth();
|
||||
/** @brief BLE reorder buffer 已缓存的乱序包数(每个 slot 持有 malloc 的 payload)。
|
||||
* 供 /system/heap_low_log 采样。非 ESP32 OTA 期返回 0。 */
|
||||
uint32_t ble_ota_get_reorder_cached_count();
|
||||
|
||||
#endif // BLE_OTA_H
|
||||
|
||||
@@ -62,15 +62,14 @@ uint16_t ble_ota_log_conn_params(uint16_t conn_handle, const char *phase)
|
||||
struct ble_gap_conn_desc desc = {};
|
||||
int rc = ble_gap_conn_find(conn_handle, &desc);
|
||||
if (rc != 0) {
|
||||
DEBUG_WARN(TAG, "%s读取 BLE 连接参数失败: rc=%d", phase, rc);
|
||||
DEBUG_WARN(TAG, "%sBLE param读失败 rc=%d", phase, rc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "%sBLE 连接参数: interval=%.2fms latency=%u supervision=%ums",
|
||||
DEBUG_DBG(TAG, "%sitv=%.2f lat=%u",
|
||||
phase,
|
||||
desc.conn_itvl * 1.25f,
|
||||
(unsigned)desc.conn_latency,
|
||||
(unsigned)desc.supervision_timeout * 10U);
|
||||
(unsigned)desc.conn_latency);
|
||||
return desc.conn_itvl;
|
||||
}
|
||||
|
||||
@@ -97,12 +96,10 @@ int ble_ota_request_conn_params(uint16_t conn_handle, bool windows_compatible)
|
||||
params.supervision_timeout = 200;
|
||||
|
||||
int rc = ble_gap_update_params(conn_handle, ¶ms);
|
||||
DEBUG_DBG(TAG, "请求 BLE 连接参数(%s): interval=%.1f..%.1fms ce=%u..%u rc=%d",
|
||||
windows_compatible ? "Windows兼容" : "高速",
|
||||
DEBUG_DBG(TAG, "BLEp(%s) %.1f-%.1f rc=%d",
|
||||
windows_compatible ? "Win" : "Hi",
|
||||
params.itvl_min * 1.25f,
|
||||
params.itvl_max * 1.25f,
|
||||
(unsigned)params.min_ce_len,
|
||||
(unsigned)params.max_ce_len,
|
||||
rc);
|
||||
return rc;
|
||||
}
|
||||
@@ -129,11 +126,8 @@ void esp32_reorder_alloc()
|
||||
s_fallback_ooo_count = 0;
|
||||
|
||||
if (!s_reorder.slots || !s_reorder.lens) {
|
||||
DEBUG_WARN(TAG, "Reorder buffer 分配失败 (slots=%p lens=%p),回退到丢弃策略。"
|
||||
"heap: free=%u min=%u",
|
||||
s_reorder.slots, s_reorder.lens,
|
||||
(unsigned)esp_get_free_heap_size(),
|
||||
(unsigned)esp_get_minimum_free_heap_size());
|
||||
DEBUG_WARN(TAG, "Reorder分配失败,回退丢弃 free=%u",
|
||||
(unsigned)esp_get_free_heap_size());
|
||||
// 释放可能部分成功的分配
|
||||
free(s_reorder.slots); s_reorder.slots = nullptr;
|
||||
free(s_reorder.lens); s_reorder.lens = nullptr;
|
||||
@@ -143,7 +137,7 @@ void esp32_reorder_alloc()
|
||||
}
|
||||
s_reorder.active = true;
|
||||
s_reorder.fallback = false;
|
||||
DEBUG_DBG(TAG, "Reorder buffer 已启用: window=%u payloadCap=%u heap=%u",
|
||||
DEBUG_DBG(TAG, "Reorder ok win=%u cap=%u heap=%u",
|
||||
(unsigned)s_reorder.windowSize, (unsigned)s_reorder.payloadCap,
|
||||
(unsigned)esp_get_free_heap_size());
|
||||
}
|
||||
@@ -178,6 +172,16 @@ void esp32_reorder_free()
|
||||
s_fallback_ooo_count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 取当前 reorder buffer 已缓存的乱序包数(每个 slot 持有 malloc 的 payload)。
|
||||
* 供堆低水位快照采样,定位"OTA 期低谷是不是 BLE reorder 引起"。
|
||||
* 非 OTA 期 active=false,返回 0。
|
||||
*/
|
||||
uint32_t ble_ota_get_reorder_cached_count()
|
||||
{
|
||||
return s_reorder.active ? s_reorder.cachedCount : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief flush expected 对应的连续缓存包到 flash,并释放对应 slot。
|
||||
* 由 esp32_write_ordered 在每次 expected 推进后调用。
|
||||
@@ -249,14 +253,14 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
|
||||
if (shouldAck) {
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_WARN(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),节流 ACK bytes=%u (ooo=%u)",
|
||||
DEBUG_WARN(TAG, "乱序 s=%u e=%u ACK=%u ooo=%u",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)curReceived,
|
||||
(unsigned)s_fallback_ooo_count);
|
||||
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
s_fallback_ooo_count = 0;
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "ESP32 乱序包跳过(回退): seq=%u (expected=%u),抑制 ACK (ooo=%u)",
|
||||
DEBUG_DBG(TAG, "ESP32乱序 s=%u e=%u 抑ACK ooo=%u",
|
||||
seq, (unsigned)s_reorder.expected,
|
||||
(unsigned)s_fallback_ooo_count);
|
||||
}
|
||||
@@ -289,7 +293,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
s_reorder.expected++;
|
||||
uint32_t flushed = esp32_reorder_flush();
|
||||
if (flushed > 0) {
|
||||
DEBUG_INFO(TAG, "Reorder 缺口已补齐: seq=%u,连续 flush=%u 包,expected→%u",
|
||||
DEBUG_INFO(TAG, "Reorder补齐 seq=%u flush=%u exp→%u",
|
||||
seq, (unsigned)flushed, (unsigned)s_reorder.expected);
|
||||
}
|
||||
return ESP_OK;
|
||||
@@ -300,7 +304,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
if (idx < s_reorder.windowSize) {
|
||||
if (s_reorder.slots[idx]) {
|
||||
// 该 slot 已被占用(重复的乱序包),跳过
|
||||
DEBUG_DBG(TAG, "Reorder slot 已占: seq=%u idx=%u,跳过重复包",
|
||||
DEBUG_DBG(TAG, "Reorder占 s=%u i=%u 跳过",
|
||||
seq, (unsigned)idx);
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -309,8 +313,7 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
uint8_t *copy = (uint8_t *)malloc(data_len);
|
||||
if (!copy) {
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_ERR(TAG, "Reorder slot malloc 失败: seq=%u expected=%u bytes=%u,"
|
||||
"ACK 报告缺口并 abort",
|
||||
DEBUG_ERR(TAG, "Reorder OOM s=%u e=%u n=%u ACK+abort",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)curReceived);
|
||||
sendWindowAck(cmd,
|
||||
s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
@@ -322,18 +325,18 @@ static esp_err_t esp32_write_ordered(uint16_t seq, uint8_t cmd,
|
||||
s_reorder.lens[idx] = data_len;
|
||||
s_reorder.cachedCount++;
|
||||
if (s_reorder.cachedCount == 1U) {
|
||||
DEBUG_INFO(TAG, "Reorder 检测到缺口: expected=%u,开始缓存 seq=%u",
|
||||
DEBUG_INFO(TAG, "Reorder缺口 exp=%u cache seq=%u",
|
||||
(unsigned)s_reorder.expected, seq);
|
||||
}
|
||||
DEBUG_DBG(TAG, "Reorder 缓存乱序包: seq=%u idx=%u (expected=%u)",
|
||||
DEBUG_DBG(TAG, "Reorder cache s=%u i=%u exp=%u",
|
||||
seq, (unsigned)idx, (unsigned)s_reorder.expected);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// seq 超出窗口范围(防御性,理论不会发生)
|
||||
uint32_t curReceived = (uint32_t)Esp32OtaCore::instance().getReceived();
|
||||
DEBUG_WARN(TAG, "ESP32 包超出 reorder 窗口: seq=%u (expected=%u base=%u win=%u),ACK bytes=%u",
|
||||
seq, (unsigned)s_reorder.expected, (unsigned)s_reorder.windowBase,
|
||||
DEBUG_WARN(TAG, "超窗 s=%u e=%u w=%u ACK=%u",
|
||||
seq, (unsigned)s_reorder.expected,
|
||||
(unsigned)s_reorder.windowSize, (unsigned)curReceived);
|
||||
sendWindowAck(cmd, s_reorder.expected > 0 ? (uint16_t)(s_reorder.expected - 1) : 0,
|
||||
curReceived);
|
||||
@@ -448,7 +451,7 @@ void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
||||
|
||||
esp_err_t err = BleOta::instance().notifyResponse(ack, sizeof(ack));
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "发送 ACK 失败: cmd=0x%02X seq=%u err=%s",
|
||||
DEBUG_WARN(TAG, "ACK失败 cmd=0x%02X seq=%u %s",
|
||||
cmd, seq, esp_err_to_name(err));
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "已发送 ACK: cmd=0x%02X seq=%u bytes=%lu",
|
||||
@@ -530,7 +533,7 @@ void ble_gd32_relay_wait_task(void *pvParameters)
|
||||
|
||||
ota_result_t result = (ota_result_t)((int32_t)result_msg);
|
||||
if (result == OTA_RESULT_OK) {
|
||||
DEBUG_INFO(TAG, "GD32 BLE OTA 中继完成,通知前端 (stage=0x11, session=%u)", session_id);
|
||||
DEBUG_INFO(TAG, "GD32中继完成 stage=0x11 sess=%u", session_id);
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_DONE,
|
||||
packBleFinishValue(session_id, 0));
|
||||
} else {
|
||||
@@ -596,7 +599,7 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
memcpy(heap_buf, buf, (size_t)recv_len);
|
||||
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
|
||||
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
|
||||
DEBUG_WARN(TAG, "DATA_IN 工作队列满,丢弃 %d 字节(等待客户端重传)", recv_len);
|
||||
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
|
||||
free(heap_buf);
|
||||
}
|
||||
} else {
|
||||
@@ -626,12 +629,12 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
if (Esp32OtaCore::instance().isActive()) {
|
||||
ota_channel_t curCh = OtaCoordinator::instance().getOtaChannel();
|
||||
if (curCh != OTA_CHANNEL_BLE) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 被其他通道占用(ch=%d),BLE 拒绝抢占",
|
||||
DEBUG_WARN(TAG, "ESP32被ch=%d占用,BLE拒抢",
|
||||
(int)curCh);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_MUTEX, 0);
|
||||
return true;
|
||||
}
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 会话已激活,seq=0 视为重传,保留 reorder 状态");
|
||||
DEBUG_INFO(TAG, "ESP32已激活 seq=0重传,保留reorder");
|
||||
} else {
|
||||
// 分区选择/大小校验/互斥由 Esp32OtaCore::begin 内部完成
|
||||
esp_err_t err = ble_esp32_ota_begin(total_size);
|
||||
@@ -696,7 +699,7 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
isLastPacket ||
|
||||
firstPacketReady)) {
|
||||
if (isLastPacket || transferComplete) {
|
||||
DEBUG_DBG(TAG, "ESP32 尾包/完成检测: seq=%u data_len=%zu received=%u total=%" PRIu32,
|
||||
DEBUG_DBG(TAG, "ESP32尾包 s=%u n=%zu rx=%u tot=%" PRIu32,
|
||||
seq, data_len, (unsigned int)afterReceived, total_size);
|
||||
} else if (firstPacketReady) {
|
||||
DEBUG_DBG(TAG, "ESP32 begin 就绪 ACK: seq=0 received=%u total=%" PRIu32,
|
||||
@@ -709,7 +712,7 @@ static bool process_esp32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
}
|
||||
|
||||
if (transferComplete) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
DEBUG_INFO(TAG, "ESP32收完 %u/%" PRIu32 "B chunks=%u",
|
||||
(unsigned int)Esp32OtaCore::instance().getReceived(), total_size, seq + 1);
|
||||
OtaCoordinator::instance().notifyEsp32ReceiveComplete();
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
@@ -743,12 +746,12 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
// (否则会 erase 分区、清 meta,毁掉已写入进度)。
|
||||
if (s_ble_gd32_meta_active && s_ble_gd32_meta.total_size == total_size) {
|
||||
if (s_ble_gd32_meta.stored_size > 0) {
|
||||
DEBUG_INFO(TAG, "GD32 会话已激活,seq=0 视为重传,跳过写入 stored=%" PRIu32,
|
||||
DEBUG_INFO(TAG, "GD32已激活 seq=0重传 skip stored=%" PRIu32,
|
||||
s_ble_gd32_meta.stored_size);
|
||||
sendWindowAck(cmd, 0, s_ble_gd32_meta.stored_size);
|
||||
return false;
|
||||
}
|
||||
DEBUG_INFO(TAG, "GD32 会话已激活且未落盘,seq=0 重传首块(不重新 prepare)");
|
||||
DEBUG_INFO(TAG, "GD32未落盘 seq=0重传首块(不prepare)");
|
||||
// fall through:stored_size==0 时重写 offset 0
|
||||
} else {
|
||||
// 与 WiFi 共用 prepareGd32Storage:validate + begin + erase + clearMeta + saveMeta
|
||||
@@ -810,12 +813,12 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
(s_gd32_ooo_count >= BLE_OTA_ACK_WINDOW) ||
|
||||
((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U);
|
||||
if (shouldAck) {
|
||||
DEBUG_WARN(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),节流 ACK bytes=%" PRIu32 " (ooo=%u)",
|
||||
DEBUG_WARN(TAG, "GD32乱序 s=%u last=%" PRIu32 " ACK=%" PRIu32 " ooo=%u",
|
||||
seq, meta.last_seq, meta.stored_size, (unsigned)s_gd32_ooo_count);
|
||||
sendWindowAck(cmd, (uint16_t)meta.last_seq, meta.stored_size);
|
||||
s_gd32_ooo_count = 0;
|
||||
} else {
|
||||
DEBUG_DBG(TAG, "GD32 乱序包跳过: seq=%u (last_seq=%" PRIu32 "),抑制 ACK (ooo=%u)",
|
||||
DEBUG_DBG(TAG, "GD32乱序 s=%u last=%" PRIu32 " 抑ACK ooo=%u",
|
||||
seq, meta.last_seq, (unsigned)s_gd32_ooo_count);
|
||||
}
|
||||
return false; // 外层推进 offset,继续下一包
|
||||
@@ -881,14 +884,14 @@ static bool process_gd32_packet(uint8_t cmd, uint16_t seq, uint32_t total_size,
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (transfer_complete) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
DEBUG_INFO(TAG, "GD32收完 %" PRIu32 "/%" PRIu32 "B chunks=%u",
|
||||
meta.stored_size, meta.total_size, seq + 1);
|
||||
s_ble_gd32_meta_active = false;
|
||||
ble_ota_restore_wifi_ps();
|
||||
// 与 WiFi 共用 finishGd32StorageAfterReceive:complete + SHA
|
||||
esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive();
|
||||
if (sha_err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 固件 SHA-256 计算失败: %s", esp_err_to_name(sha_err));
|
||||
DEBUG_ERR(TAG, "GD32 SHA失败:%s", esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -937,8 +940,8 @@ void ble_ota_process_data_batch(const uint8_t *buf, size_t recv_len)
|
||||
// 当前包总长度: header(7) + payload(data_len)
|
||||
size_t packet_len = 7 + data_len;
|
||||
|
||||
DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu offset=%zu",
|
||||
cmd, seq, total_size, data_len, offset);
|
||||
DEBUG_DBG(TAG, "DATA_IN c=0x%02X s=%u tot=%" PRIu32 " n=%zu",
|
||||
cmd, seq, total_size, data_len);
|
||||
|
||||
// 分发到对应通道的包处理器
|
||||
bool abort_batch = false;
|
||||
@@ -985,7 +988,7 @@ void ble_ota_data_worker_task(void *pvParameters)
|
||||
// 若 BLE 已断开或 WiFi OTA 挂起,丢弃队列中的残留数据
|
||||
if (!BleOta::instance().isConnected() ||
|
||||
BleOta::instance().isSuspendedForWifiOta()) {
|
||||
DEBUG_DBG(TAG, "BLE 不可用/挂起,丢弃 %zu 字节队列数据", item.len);
|
||||
DEBUG_DBG(TAG, "BLE不可用 丢%zuB队列", item.len);
|
||||
free(item.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
if (len < 35) {
|
||||
DEBUG_WARN(TAG, "CTRL 0xF7 数据长度不足 35(前端 SHA-256 必传),拒绝触发 GD32 中继");
|
||||
DEBUG_WARN(TAG, "CTRL 0xF7 len<35 缺SHA,拒GD32中继");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
|
||||
packBleFinishValue(session_id, 1));
|
||||
return 0;
|
||||
@@ -60,22 +60,21 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha);
|
||||
shaBytesToHexUpper(device_sha, device_sha_hex);
|
||||
sendSha256Notify(device_sha, session_id);
|
||||
DEBUG_DBG(TAG, "GD32 前端 SHA-256: %s, session=%u", frontend_sha_hex, session_id);
|
||||
DEBUG_DBG(TAG, "GD32 缓存 SHA-256: %s", device_sha_hex);
|
||||
|
||||
if (err == ESP_ERR_INVALID_CRC) {
|
||||
DEBUG_ERR(TAG, "GD32 缓存 SHA-256 与前端文件不匹配,拒绝触发 SPI 中继");
|
||||
DEBUG_ERR(TAG, "GD32 SHA-256 不匹配");
|
||||
DEBUG_ERR(TAG, "frontend=%s", frontend_sha_hex);
|
||||
DEBUG_ERR(TAG, "device=%s", device_sha_hex);
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SHA_MISMATCH,
|
||||
packBleFinishValue(session_id, 0));
|
||||
return 0;
|
||||
} else if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "GD32 SHA-256 计算/读取失败: %s", esp_err_to_name(err));
|
||||
DEBUG_ERR(TAG, "GD32 SHA 失败: %s", esp_err_to_name(err));
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 3));
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "GD32 固件就绪且 SHA-256 匹配 (%" PRIu32 " / %" PRIu32 " 字节),通过协调器触发中继",
|
||||
DEBUG_INFO(TAG, "GD32就绪 SHA匹配 %" PRIu32 "/%" PRIu32 ",触发中继",
|
||||
meta.stored_size, meta.total_size);
|
||||
if (OtaRelay::getResultQueue()) {
|
||||
uint32_t stale = 0;
|
||||
@@ -93,7 +92,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
sendFinishEvent(BLE_OTA_FINISH_GD32_RELAY_START,
|
||||
packBleFinishValue(session_id, 0));
|
||||
DEBUG_INFO(TAG, "已通知前端: GD32 中继已开始 (stage=0x10, session=%u)", session_id);
|
||||
DEBUG_INFO(TAG, "GD32中继开始 stage=0x10 sess=%u", session_id);
|
||||
|
||||
BleGd32RelayWaitCtx *ctx = (BleGd32RelayWaitCtx *)malloc(sizeof(BleGd32RelayWaitCtx));
|
||||
if (!ctx) {
|
||||
@@ -142,7 +141,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
// 强制要求 32B 前端 SHA-256:与 WiFi 流程对齐,设备端读回分区计算并比对,
|
||||
// 一致走 end+set_boot+reboot,不一致返回 0x85 ERR_SHA_MISMATCH。
|
||||
if (len < 33) {
|
||||
DEBUG_WARN(TAG, "CTRL 0xF8 数据长度不足 33(前端 SHA-256 必传),拒绝触发");
|
||||
DEBUG_WARN(TAG, "CTRL 0xF8 len<33 缺SHA,拒绝");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_FINALIZE, (uint32_t)ESP_ERR_INVALID_ARG);
|
||||
return 0;
|
||||
}
|
||||
@@ -159,7 +158,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
finish_ctx->sha_hex[64] = '\0';
|
||||
finish_ctx->session_id = session_id;
|
||||
DEBUG_DBG(TAG, "前端 SHA-256: %s, session=%u", finish_ctx->sha_hex, session_id);
|
||||
DEBUG_DBG(TAG, "feSHA=%s sess=%u", finish_ctx->sha_hex, session_id);
|
||||
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||
@@ -179,7 +178,7 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_RESTART_ESP32) {
|
||||
// 重启 ESP32:GATT 回调中仅推送 FINISH,延时 + 重启交给独立 task,避免卡 NimBLE host
|
||||
DEBUG_INFO(TAG, "收到 0xFA 重启请求,创建 restart task(500ms 后 esp_restart())");
|
||||
DEBUG_INFO(TAG, "0xFA重启,建restart task(500ms)");
|
||||
if (BleOta::instance().m_esp32RebootPending) {
|
||||
DEBUG_WARN(TAG, "0xFA 重启已在进行中,忽略重复 CTRL");
|
||||
return 0;
|
||||
@@ -207,11 +206,11 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
ota_system_state_t cur_state = OtaCoordinator::instance().getSystemState();
|
||||
DEBUG_WARN(TAG, "收到 0xFB 中止请求: state=%s session=%u",
|
||||
DEBUG_WARN(TAG, "0xFB abort state=%s sess=%u",
|
||||
OtaCoordinator::stateToString(cur_state), session_id);
|
||||
|
||||
if (cur_state == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "GD32 SPI 中继进行中,无法中止(请等待完成或断开 BLE)");
|
||||
DEBUG_WARN(TAG, "GD32 SPI中继中,无法中止,请等待");
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_GD32_RELAY,
|
||||
packBleFinishValue(session_id, 2));
|
||||
return 0;
|
||||
@@ -466,7 +465,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
{
|
||||
ota_system_state_t cur = OtaCoordinator::instance().getSystemState();
|
||||
if (cur == OTA_SYS_GD32_RELAYING || cur == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "BLE 断开但 %s 进行中,保留状态等待自然结束",
|
||||
DEBUG_WARN(TAG, "BLE断但%s进行中,保留状态",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
} else if (cur == OTA_SYS_ESP32_RECEIVING ||
|
||||
cur == OTA_SYS_ESP32_FINALIZING ||
|
||||
@@ -479,7 +478,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
ch = snap.channel;
|
||||
}
|
||||
if (ch == OTA_CHANNEL_BLE) {
|
||||
DEBUG_WARN(TAG, "BLE 断开,中止 BLE 通道 OTA (state=%s)",
|
||||
DEBUG_WARN(TAG, "BLE断,中止OTA state=%s",
|
||||
OtaCoordinator::stateToString(cur));
|
||||
ble_ota_session_reset();
|
||||
} else {
|
||||
@@ -502,7 +501,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
return 0;
|
||||
|
||||
case BLE_GAP_EVENT_SUBSCRIBE:
|
||||
DEBUG_INFO(TAG, "CCC 描述符更新: conn=%d attr=%d notify=%d indicate=%d",
|
||||
DEBUG_INFO(TAG, "CCC upd conn=%d attr=%d n=%d i=%d",
|
||||
event->subscribe.conn_handle,
|
||||
event->subscribe.attr_handle,
|
||||
event->subscribe.cur_notify,
|
||||
@@ -543,7 +542,7 @@ int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
const char *tx_phy = (tx == BLE_GAP_LE_PHY_2M) ? "2M" :
|
||||
(tx == BLE_GAP_LE_PHY_1M) ? "1M" :
|
||||
(tx == BLE_GAP_LE_PHY_CODED) ? "CODED" : "?";
|
||||
DEBUG_INFO(TAG, "PHY 更新: status=%d rx=%s tx=%s",
|
||||
DEBUG_INFO(TAG, "PHY %d %s/%s",
|
||||
event->phy_updated.status, rx_phy, tx_phy);
|
||||
return 0;
|
||||
}
|
||||
@@ -573,18 +572,18 @@ void ble_ota_start_advertising(void) // kept as free function for BLE callback
|
||||
uint8_t own_addr_type;
|
||||
rc = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "推断地址类型失败: rc=%d, 尝试生成随机地址", rc);
|
||||
DEBUG_ERR(TAG, "推断地址失败 rc=%d,试随机地址", rc);
|
||||
// 生成不可解析随机地址作为 fallback
|
||||
ble_addr_t rnd_addr;
|
||||
rc = ble_hs_id_gen_rnd(0, &rnd_addr);
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "生成随机地址也失败: rc=%d, BLE 广播无法启动", rc);
|
||||
DEBUG_ERR(TAG, "随机地址失败 rc=%d", rc);
|
||||
return;
|
||||
}
|
||||
own_addr_type = rnd_addr.type;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "使用地址类型: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random");
|
||||
DEBUG_DBG(TAG, "BLE addr type: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random");
|
||||
|
||||
// --- 构建广播数据 ---
|
||||
uint8_t adv_data[31] = {0};
|
||||
@@ -640,7 +639,8 @@ void ble_ota_start_advertising(void) // kept as free function for BLE callback
|
||||
if (rc != 0) {
|
||||
DEBUG_ERR(TAG, "启动广播失败: %d", rc);
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "BLE 广播已启动: \"%s\"", name);
|
||||
/* 冷启动与 WiFi 共存恢复都会走这里;常态 INFO 不刷,避免上传日志夹杂 BLE */
|
||||
DEBUG_DBG(TAG, "BLE广播就绪: \"%s\"", name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+455
-167
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
#include <algorithm>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_rom_sys.h"
|
||||
#include "soc/soc.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#include "app_config.h"
|
||||
@@ -32,7 +34,8 @@
|
||||
#include "spi_master.h"
|
||||
#include "canfd_bridge.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
#include "monitor/task_stack_check.h"
|
||||
#include "monitor/gd32_adc_feedback.h"
|
||||
#include "monitor/task_stack.h"
|
||||
#include "ble_ota.h"
|
||||
#include "system_boot.h"
|
||||
|
||||
@@ -235,7 +238,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
http_set_cors_headers(req);
|
||||
|
||||
uint8_t *recv_buf = s_ota_upload_recv;
|
||||
DEBUG_WARN("WIFI", "=== HTTP: POST /ota/upload === content_len=%d free=%lu largest=%lu",
|
||||
DEBUG_WARN("WIFI", "POST /ota/upload clen=%d free=%lu big=%lu",
|
||||
req->content_len,
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
@@ -318,7 +321,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
size_t total_firmware_written = 0;
|
||||
size_t total_received = json_header_len;
|
||||
|
||||
DEBUG_INFO("WIFI", "ESP32 OTA 开始收流: content_len=%d json_hdr=%d fw_expect=%d",
|
||||
DEBUG_INFO("WIFI", "ESP32收流 clen=%d hdr=%d exp=%d",
|
||||
req->content_len, json_header_len,
|
||||
(req->content_len > json_header_len)
|
||||
? (req->content_len - json_header_len) : 0);
|
||||
@@ -338,7 +341,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
|
||||
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
|
||||
if (ret <= 0) {
|
||||
DEBUG_ERR("WIFI", "HTTP recv failed (ret=%d), aborting OTA recv=%u/%d free=%lu",
|
||||
DEBUG_ERR("WIFI", "HTTP recv fail ret=%d OTA %u/%d free=%lu",
|
||||
ret, (unsigned)total_received, req->content_len,
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch);
|
||||
@@ -377,10 +380,9 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
esp_err_t send_err = httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
||||
if (send_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "ESP32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
||||
DEBUG_ERR("WIFI", "ESP32 OTA回包失败: %s free=%lu",
|
||||
esp_err_to_name(send_err),
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
/* 再试一次极简回包 */
|
||||
send_err = httpd_resp_send(req, "{\"status\":\"ok\",\"target\":\"esp32\"}",
|
||||
HTTPD_RESP_USE_STRLEN);
|
||||
@@ -388,8 +390,7 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
DEBUG_ERR("WIFI", "ESP32 OTA 简短响应仍失败: %s", esp_err_to_name(send_err));
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("WIFI",
|
||||
"ESP32 OTA 存储+响应完成: %u B elapsed=%lld ms free=%lu",
|
||||
DEBUG_INFO("WIFI", "ESP32存完 %uB %lldms free=%lu",
|
||||
(unsigned int)core.getReceived(),
|
||||
(long long)((esp_timer_get_time() - t_recv0) / 1000),
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
@@ -505,11 +506,8 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
if (sha_err != ESP_OK) {
|
||||
/* 存盘已完成,仍回 status=ok;relay 阶段会再比前端 SHA。
|
||||
* 此处必须醒目告警,避免只当 INFO 被忽略。 */
|
||||
DEBUG_WARN("WIFI",
|
||||
"GD32 上传完成但 SHA-256 预计算失败: %s(仍返回 stored ok,/ota/relay 将再校验)",
|
||||
DEBUG_WARN("WIFI", "GD32 SHA预算失败:%s (relay再校)",
|
||||
esp_err_to_name(sha_err));
|
||||
DEBUG_ERR("WIFI", "GD32 finishGd32StorageAfterReceive failed: %s",
|
||||
esp_err_to_name(sha_err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,10 +517,9 @@ esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
esp_err_t send_err = httpd_resp_send(req, resp, strlen(resp));
|
||||
if (send_err != ESP_OK) {
|
||||
DEBUG_ERR("WIFI", "GD32 OTA 响应发送失败: %s (free=%lu largest=%lu)",
|
||||
DEBUG_ERR("WIFI", "GD32 OTA回包失败: %s free=%lu",
|
||||
esp_err_to_name(send_err),
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
(unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
|
||||
(unsigned long)esp_get_free_heap_size());
|
||||
} else {
|
||||
DEBUG_INFO("WIFI", "GD32 OTA 存储+响应完成: %u / %u free=%lu",
|
||||
(unsigned)gd32_offset, expected_fw_size_raw,
|
||||
@@ -581,6 +578,11 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
}
|
||||
|
||||
if (verify_err == ESP_ERR_INVALID_CRC) {
|
||||
DEBUG_ERR("OTA", "GD32 SHA-256 不匹配");
|
||||
DEBUG_ERR("OTA", "frontend=%s", frontend_sha_hex);
|
||||
DEBUG_ERR("OTA", "device=%s", device_sha_hex);
|
||||
/* 未进中继:须恢复 WiFi 上传期挂起的 BLE */
|
||||
(void)BleOta::instance().resumeStack();
|
||||
char err_resp[256];
|
||||
snprintf(err_resp, sizeof(err_resp),
|
||||
"{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", "
|
||||
@@ -589,12 +591,14 @@ esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
|
||||
httpd_resp_send(req, err_resp, strlen(err_resp));
|
||||
return ESP_FAIL;
|
||||
} else if (verify_err != ESP_OK) {
|
||||
(void)BleOta::instance().resumeStack();
|
||||
return http_send_json_error(req, "500 Internal Server Error",
|
||||
"sha_compute_failed", "SHA compute failed");
|
||||
}
|
||||
|
||||
esp_err_t trigger_err = OtaCoordinator::instance().triggerGd32Relay();
|
||||
if (trigger_err != ESP_OK) {
|
||||
(void)BleOta::instance().resumeStack();
|
||||
return http_send_json_error(req, "400 Bad Request",
|
||||
"relay_trigger_failed",
|
||||
"relay trigger failed - system busy or firmware not ready");
|
||||
@@ -630,8 +634,53 @@ esp_err_t logs_handler(httpd_req_t *req)
|
||||
|
||||
|
||||
// ============================================================
|
||||
// GET /can/feedback — 三路 CAN 反馈快照(GD32 推送,ESP32 缓存)
|
||||
// HTTP JSON 共享工作区(.bss 单份 ~3KB)
|
||||
// 供 /system/status、/can/feedback、/system/heap_low_log 串行复用;
|
||||
// 互斥保护,禁止并发写同一缓冲(httpd 多 worker 时会排队)。
|
||||
// ============================================================
|
||||
static constexpr size_t HTTP_JSON_WORK_CAP = 3072;
|
||||
static char s_http_json_work[HTTP_JSON_WORK_CAP];
|
||||
static SemaphoreHandle_t s_http_json_mux = nullptr;
|
||||
static portMUX_TYPE s_http_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
static SemaphoreHandle_t httpJsonWorkMutex(void)
|
||||
{
|
||||
if (s_http_json_mux == nullptr) {
|
||||
portENTER_CRITICAL(&s_http_json_init_mux);
|
||||
if (s_http_json_mux == nullptr) {
|
||||
s_http_json_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
portEXIT_CRITICAL(&s_http_json_init_mux);
|
||||
}
|
||||
return s_http_json_mux;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /can/feedback — 三路 CAN 反馈快照(GD32 推送,ESP32 缓存)
|
||||
// 分块发送:复用 s_http_json_work,一次 HTTP 内按通道 send_chunk;
|
||||
// 前端仍一次 fetch 收完整 JSON。
|
||||
// ============================================================
|
||||
/** 监控预览:data hex 最多 8 字节(16 字符),避免 FD 64B 撑爆分块 */
|
||||
static constexpr uint8_t FEEDBACK_DATA_HEX_MAX = 8;
|
||||
|
||||
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;
|
||||
}
|
||||
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 */
|
||||
}
|
||||
return off + n;
|
||||
}
|
||||
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
@@ -642,20 +691,61 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
valid[i] = getLatestCanFeedback(i, &results[i]);
|
||||
}
|
||||
|
||||
/* 静态响应缓冲,减少频繁大块堆分配 */
|
||||
static char s_feedback_json[8192];
|
||||
char *resp = s_feedback_json;
|
||||
memset(resp, 0, sizeof(s_feedback_json));
|
||||
const bool feat_known = SpiMaster::instance().gd32FeatKnown();
|
||||
const uint32_t feat = SpiMaster::instance().gd32FeatFlags();
|
||||
const bool ch_en[CAN_CHANNEL_MAX] = {
|
||||
!feat_known || (feat & V2_FEAT_CAN_CH0) != 0U,
|
||||
!feat_known || (feat & V2_FEAT_CAN_CH1) != 0U,
|
||||
!feat_known || (feat & V2_FEAT_CAN_CH2) != 0U,
|
||||
};
|
||||
const unsigned ch_mask = feat_known
|
||||
? (unsigned)((feat & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT)
|
||||
: 0x7U;
|
||||
|
||||
SemaphoreHandle_t mux = httpJsonWorkMutex();
|
||||
if (mux && xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "fb busy");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
char *work = s_http_json_work;
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
|
||||
auto fail_unlock = [&](esp_err_t err) -> esp_err_t {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return err;
|
||||
};
|
||||
|
||||
int n = snprintf(work, HTTP_JSON_WORK_CAP,
|
||||
"{\"feat_known\":%s,\"can_ch_mask\":%u,\"can_brs\":%s,\"channels\":[",
|
||||
feat_known ? "true" : "false",
|
||||
ch_mask,
|
||||
(feat_known && (feat & V2_FEAT_CAN_BRS)) ? "true" : "false");
|
||||
if (n < 0 || (size_t)n >= HTTP_JSON_WORK_CAP) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "fb hdr");
|
||||
return fail_unlock(ESP_FAIL);
|
||||
}
|
||||
if (httpd_resp_send_chunk(req, work, n) != ESP_OK) {
|
||||
return fail_unlock(ESP_FAIL);
|
||||
}
|
||||
|
||||
char *p = resp;
|
||||
p += sprintf(p, "{\"channels\":[");
|
||||
for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) {
|
||||
const can_feedback_result_t &r = results[i];
|
||||
if (i > 0) p += sprintf(p, ",");
|
||||
p += sprintf(p, "{\"ch\":%u,\"valid\":%s", (unsigned)i,
|
||||
valid[i] ? "true" : "false");
|
||||
if (valid[i]) {
|
||||
p += sprintf(p,
|
||||
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");
|
||||
if (off < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (valid[i] && ch_en[i]) {
|
||||
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,"
|
||||
"\"bus_state\":%u,\"count\":%u,\"entries\":[",
|
||||
@@ -669,33 +759,74 @@ esp_err_t can_feedback_handler(httpd_req_t *req)
|
||||
(unsigned)r.stats.rec,
|
||||
(unsigned)r.stats.bus_state,
|
||||
(unsigned)r.total_count);
|
||||
if (off < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t emitted = 0;
|
||||
for (uint8_t j = 0; j < r.total_count && j < CANFD_ID_SNAPSHOT_MAX; ++j) {
|
||||
const id_snapshot_entry_t &e = r.entries[j];
|
||||
if (j > 0) p += sprintf(p, ",");
|
||||
p += sprintf(p,
|
||||
"{\"id\":\"0x%08lX\",\"dlc\":%u,\"fd\":%u,"
|
||||
uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : (uint8_t)CANFD_ID_SNAPSHOT_DATA_MAX;
|
||||
if (dataLen > FEEDBACK_DATA_HEX_MAX) {
|
||||
dataLen = FEEDBACK_DATA_HEX_MAX;
|
||||
}
|
||||
|
||||
/* 先估长度:装不下则截断 entries(count 仍报真实 total) */
|
||||
char entry_tmp[96 + FEEDBACK_DATA_HEX_MAX * 2];
|
||||
int elen = snprintf(entry_tmp, sizeof(entry_tmp),
|
||||
"%s{\"id\":\"0x%08lX\",\"dlc\":%u,\"fd\":%u,"
|
||||
"\"brs\":%u,\"esi\":%u,\"data\":\"",
|
||||
emitted ? "," : "",
|
||||
(unsigned long)e.id,
|
||||
(unsigned)e.dlc,
|
||||
(unsigned)e.fd_flag,
|
||||
(unsigned)e.brs,
|
||||
(unsigned)e.esi);
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : CANFD_ID_SNAPSHOT_DATA_MAX;
|
||||
for (uint8_t d = 0; d < dataLen; ++d) {
|
||||
p += sprintf(p, "%02X", e.data[d]);
|
||||
if (elen < 0 || (size_t)elen >= sizeof(entry_tmp)) {
|
||||
break;
|
||||
}
|
||||
p += sprintf(p, "\"}");
|
||||
}
|
||||
p += sprintf(p, "]");
|
||||
}
|
||||
p += sprintf(p, "}");
|
||||
}
|
||||
p += sprintf(p, "]}");
|
||||
for (uint8_t d = 0; d < dataLen; ++d) {
|
||||
int hn = snprintf(entry_tmp + elen, sizeof(entry_tmp) - (size_t)elen,
|
||||
"%02X", e.data[d]);
|
||||
if (hn != 2) {
|
||||
break;
|
||||
}
|
||||
elen += hn;
|
||||
}
|
||||
if (elen + 2 >= (int)sizeof(entry_tmp)) {
|
||||
break;
|
||||
}
|
||||
entry_tmp[elen++] = '"';
|
||||
entry_tmp[elen++] = '}';
|
||||
entry_tmp[elen] = '\0';
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, (int)(p - resp));
|
||||
return ESP_OK;
|
||||
if (off + elen + 2 >= (int)HTTP_JSON_WORK_CAP) {
|
||||
break; /* 本通道 entries 截断 */
|
||||
}
|
||||
memcpy(work + off, entry_tmp, (size_t)elen);
|
||||
off += elen;
|
||||
emitted++;
|
||||
}
|
||||
off = feedback_append(work, HTTP_JSON_WORK_CAP, off, "]");
|
||||
if (off < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
off = feedback_append(work, HTTP_JSON_WORK_CAP, off, "}");
|
||||
if (off < 0) {
|
||||
break;
|
||||
}
|
||||
if (httpd_resp_send_chunk(req, work, off) != ESP_OK) {
|
||||
return fail_unlock(ESP_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
if (httpd_resp_send_chunk(req, "]}", 2) != ESP_OK) {
|
||||
return fail_unlock(ESP_FAIL);
|
||||
}
|
||||
esp_err_t end_err = httpd_resp_send_chunk(req, NULL, 0);
|
||||
return fail_unlock(end_err == ESP_OK ? ESP_OK : ESP_FAIL);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -767,79 +898,13 @@ esp_err_t can_mode_handler(httpd_req_t *req)
|
||||
httpd_resp_send(req, resp, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t can_send_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
char buf[256] = {0};
|
||||
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
buf[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
|
||||
cJSON *id_item = cJSON_GetObjectItem(root, "id");
|
||||
cJSON *data_item = cJSON_GetObjectItem(root, "data");
|
||||
cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
|
||||
|
||||
if (!ch_item || !id_item) {
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
|
||||
uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
|
||||
uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
|
||||
|
||||
uint8_t tx_data[CAN_MAX_DLC] = {0};
|
||||
uint8_t tx_dlc = 0;
|
||||
|
||||
if (data_item && cJSON_IsString(data_item)) {
|
||||
tx_dlc = (uint8_t)parseHexToBytes(cJSON_GetStringValue(data_item), tx_data, CAN_MAX_DLC);
|
||||
}
|
||||
|
||||
if (tx_dlc == 0 && dlc > 0) {
|
||||
tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
|
||||
}
|
||||
cJSON_Delete(root);
|
||||
|
||||
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), 2000, &gd32_err);
|
||||
|
||||
char resp[512];
|
||||
if (resp_len >= 0) {
|
||||
char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
|
||||
bytesToHexUpper(resp_data, (size_t)resp_len, resp_hex);
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
|
||||
ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"error\", \"code\": %d, \"gd32_err\": %u}",
|
||||
resp_len, (unsigned)gd32_err);
|
||||
}
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, strlen(resp));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief POST /can/send_batch — 同通道连续发送多帧,单次 HTTP 完成。
|
||||
* @brief POST /can/send_batch — CAN 发送唯一入口(单帧 = frames 长度 1)。
|
||||
*
|
||||
* JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] }
|
||||
* 返回: { "status":"ok", "results":[ ... ], 可选 skipped/abort_* }
|
||||
* 帧间间隔 kInterFrameDelayMs;批量期间暂停 CAN 反馈任务。
|
||||
* 单帧 canFdSend 超时 2000ms(等应答);多帧 80ms/帧 + 帧间隔。
|
||||
* 发送期间暂停 CAN 反馈任务。
|
||||
*/
|
||||
esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
{
|
||||
@@ -848,6 +913,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
constexpr int kMaxBody = 49152;
|
||||
constexpr int kMaxFrames = 128;
|
||||
constexpr uint32_t kPerFrameTimeoutMs = 80U;
|
||||
constexpr uint32_t kSingleFrameTimeoutMs = 2000U;
|
||||
constexpr uint32_t kInterFrameDelayMs = 5U;
|
||||
|
||||
int content_len = req->content_len;
|
||||
@@ -893,6 +959,8 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
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();
|
||||
@@ -905,7 +973,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
cJSON_AddStringToObject(out_root, "status", "ok");
|
||||
cJSON_AddItemToObject(out_root, "results", results);
|
||||
|
||||
/* 批量发送期间暂停 CAN 反馈,独占 SPI 业务路径 */
|
||||
/* 发送期间暂停 CAN 反馈,独占 SPI 业务路径 */
|
||||
pauseCanFeedbackTask(true);
|
||||
|
||||
auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) {
|
||||
@@ -962,7 +1030,7 @@ esp_err_t can_send_batch_handler(httpd_req_t *req)
|
||||
memset(resp_data, 0, sizeof(resp_data));
|
||||
uint8_t gd32_err = 0U;
|
||||
int resp_len = canFdSend(ch, ext_id, tx_data, tx_dlc,
|
||||
resp_data, sizeof(resp_data), kPerFrameTimeoutMs, &gd32_err);
|
||||
resp_data, sizeof(resp_data), frame_timeout_ms, &gd32_err);
|
||||
|
||||
if (resp_len >= 0) {
|
||||
char resp_hex[CAN_MAX_DLC * 2 + 1];
|
||||
@@ -1073,8 +1141,9 @@ esp_err_t spi_send_handler(httpd_req_t *req)
|
||||
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);
|
||||
DEBUG_INFO("SPI", "WEB SPI 响应: type=0x%02X len=%zu payload=%s",
|
||||
resp_type, resp_len, resp_hex);
|
||||
/* 日志只记 type/len,完整 payload 仅在 HTTP JSON 返回给前端 */
|
||||
DEBUG_INFO("SPI", "WEB SPI ok: type=0x%02X len=%zu",
|
||||
resp_type, resp_len);
|
||||
} else {
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"status\": \"error\", \"code\": %d, \"message\": \"%s\"}",
|
||||
@@ -1088,25 +1157,27 @@ esp_err_t spi_send_handler(httpd_req_t *req)
|
||||
}
|
||||
|
||||
static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
/* 状态 JSON 用静态缓冲:OTA 后 largest free 常 <2KB,calloc(8KB) 会 500 "no memory"。 */
|
||||
/* 含 boot 模块列表与 CPU 任务;OTA 后 largest free 常偏紧,仍用静态缓冲避免 calloc */
|
||||
static constexpr size_t SYSTEM_STATUS_JSON_CAP = 7168;
|
||||
static char s_status_json[SYSTEM_STATUS_JSON_CAP];
|
||||
static SemaphoreHandle_t s_status_json_mux = nullptr;
|
||||
static portMUX_TYPE s_status_json_init_mux = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
static SemaphoreHandle_t statusJsonMutex(void)
|
||||
/** 发送当前工作区并清空 offset(分块;失败返回 false) */
|
||||
static bool status_flush_chunk(httpd_req_t *req, char *buf, size_t cap, int *len)
|
||||
{
|
||||
if (s_status_json_mux == nullptr) {
|
||||
portENTER_CRITICAL(&s_status_json_init_mux);
|
||||
if (s_status_json_mux == nullptr) {
|
||||
s_status_json_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
portEXIT_CRITICAL(&s_status_json_init_mux);
|
||||
if (!req || !buf || !len || *len <= 0) {
|
||||
return true;
|
||||
}
|
||||
return s_status_json_mux;
|
||||
if (httpd_resp_send_chunk(req, buf, *len) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
*len = 0;
|
||||
if (cap > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* CPU 快照静态缓冲:替代每秒 calloc(1.2KB),消除 IDLE 期持续的堆抖动。
|
||||
* 与 s_http_json_work 同由 httpJsonWorkMutex 串行化。 */
|
||||
static CpuUsageSnapshot s_cpu_snapshot;
|
||||
|
||||
static size_t get_task_stack_high_watermark_bytes(TaskHandle_t handle)
|
||||
{
|
||||
if (handle == NULL) return 0;
|
||||
@@ -1136,9 +1207,10 @@ static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...)
|
||||
return offset + (int)written;
|
||||
}
|
||||
|
||||
/** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样) */
|
||||
/** 将 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,
|
||||
"\"cpu\":{\"available\":%s,\"valid\":%s,\"usage_pct\":%.2f,\"idle_pct\":%.2f,\"sample_ms\":%lu,\"tasks\":[",
|
||||
cpu.available ? "true" : "false",
|
||||
@@ -1147,7 +1219,9 @@ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUs
|
||||
cpu.valid ? cpu.idle_pct : 0.0,
|
||||
(unsigned long)(cpu.valid ? cpu.sample_ms : 0U));
|
||||
|
||||
for (size_t i = 0; i < cpu.task_count; ++i) {
|
||||
const size_t ntask =
|
||||
(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,
|
||||
"%s{\"name\":\"%s\",\"task_number\":%lu,\"runtime_delta\":%" PRIu64 ",\"usage_pct\":%.2f}",
|
||||
@@ -1166,6 +1240,20 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 堆极紧时仍返回精简 JSON,禁止 500(前端轮询会刷红)。 */
|
||||
const size_t largest_now = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||
if (largest_now < 512U) {
|
||||
@@ -1184,10 +1272,10 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
SemaphoreHandle_t mux = statusJsonMutex();
|
||||
SemaphoreHandle_t mux = httpJsonWorkMutex();
|
||||
if (mux) {
|
||||
if (xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
|
||||
/* 并发 status 冲突:仍回简包,避免 200 */
|
||||
/* 与 feedback/heap_low 争用工作区:仍回简包,避免 500 刷红 */
|
||||
char busy[160];
|
||||
snprintf(busy, sizeof(busy),
|
||||
"{\"version\":\"%s\",\"ota\":{\"sys_state\":\"busy\"},"
|
||||
@@ -1229,10 +1317,58 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
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);
|
||||
|
||||
/* 内存全景:堆只是 SRAM 中可 malloc 的部分。
|
||||
* 静态段用链接符号拆分(与 map 文件一致):
|
||||
* [IRAM code | .data | .bss | heap ...] (C3 上 IRAM/DRAM 同一物理 SRAM)
|
||||
* 手册 400KB = DRAM 窗口 + 约 16KB 指令 cache(cache 不在 SOC_DRAM_* 窗口内)。 */
|
||||
multi_heap_info_t internal_info = {};
|
||||
heap_caps_get_info(&internal_info, INTERNAL_HEAP_CAPS);
|
||||
|
||||
extern char _iram_start[];
|
||||
extern char _iram_end[];
|
||||
extern char _data_start[];
|
||||
extern char _data_end[];
|
||||
extern char _bss_start[];
|
||||
extern char _bss_end[];
|
||||
extern char _heap_start[];
|
||||
auto sym_span = [](const char *a, const char *b) -> size_t {
|
||||
const uintptr_t ua = reinterpret_cast<uintptr_t>(a);
|
||||
const uintptr_t ub = reinterpret_cast<uintptr_t>(b);
|
||||
return (ub >= ua) ? static_cast<size_t>(ub - ua) : 0U;
|
||||
};
|
||||
const size_t iram_bytes = sym_span(_iram_start, _iram_end);
|
||||
const size_t data_bytes = sym_span(_data_start, _data_end);
|
||||
const size_t bss_bytes = sym_span(_bss_start, _bss_end);
|
||||
/* C3 DIRAM:DRAM 基址起至 _heap_start 即为 IRAM+.data+.bss 占位 */
|
||||
const uintptr_t heap_start_addr = reinterpret_cast<uintptr_t>(_heap_start);
|
||||
const size_t static_to_heap =
|
||||
(heap_start_addr >= static_cast<uintptr_t>(SOC_DRAM_LOW))
|
||||
? static_cast<size_t>(heap_start_addr - static_cast<uintptr_t>(SOC_DRAM_LOW))
|
||||
: (iram_bytes + data_bytes + bss_bytes);
|
||||
|
||||
const size_t sram_soc =
|
||||
(SOC_DRAM_HIGH > SOC_DRAM_LOW) ? (size_t)(SOC_DRAM_HIGH - SOC_DRAM_LOW) : 0U;
|
||||
/* 应用实际接管:静态链接区 + 内部堆(比整窗更贴近“可用 DRAM”) */
|
||||
const size_t sram_app = static_to_heap + internal_heap_total;
|
||||
const size_t sram_total = (sram_app > 0) ? sram_app : sram_soc;
|
||||
const size_t internal_heap_used = internal_info.total_allocated_bytes;
|
||||
const size_t static_reserved = static_to_heap;
|
||||
const size_t reserved_tail =
|
||||
(sram_soc > sram_app) ? (sram_soc - sram_app) : 0U;
|
||||
const size_t dma_total = heap_caps_get_total_size(MALLOC_CAP_DMA);
|
||||
const size_t dma_free = heap_caps_get_free_size(MALLOC_CAP_DMA);
|
||||
const size_t rtc_total = heap_caps_get_total_size(MALLOC_CAP_RTCRAM);
|
||||
const size_t rtc_free = heap_caps_get_free_size(MALLOC_CAP_RTCRAM);
|
||||
|
||||
// 任务栈指标复用 health_monitor 共享的任务表(collectTaskStackMetrics),
|
||||
// 避免与巡检逻辑两地维护;http_server 在本 handler 上下文中,句柄即当前任务。
|
||||
TaskStackMetric task_metrics[12];
|
||||
size_t task_metrics_count = collectTaskStackMetrics(task_metrics, 12);
|
||||
size_t task_stack_total_bytes = 0;
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
task_stack_total_bytes += task_metrics[i].stack_size_bytes;
|
||||
}
|
||||
// http_server 行的句柄在 httpd 线程中取当前任务更准确
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
if (strcmp(task_metrics[i].name, "http_server") == 0) {
|
||||
@@ -1241,28 +1377,22 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
}
|
||||
|
||||
/* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */
|
||||
CpuUsageSnapshot cpu_local = {};
|
||||
CpuUsageSnapshot *cpu = &cpu_local;
|
||||
CpuUsageSnapshot *cpu_heap = nullptr;
|
||||
if (largest_free_block >= (sizeof(CpuUsageSnapshot) + 512U)) {
|
||||
cpu_heap = (CpuUsageSnapshot *)calloc(1, sizeof(CpuUsageSnapshot));
|
||||
if (cpu_heap) {
|
||||
cpu = cpu_heap;
|
||||
}
|
||||
}
|
||||
CpuUsageSnapshot *cpu = &s_cpu_snapshot; // 用静态缓冲,不再每秒 calloc(1.2KB)
|
||||
CpuStats::instance().getSnapshot(*cpu);
|
||||
|
||||
char *resp = s_status_json;
|
||||
memset(resp, 0, SYSTEM_STATUS_JSON_CAP);
|
||||
char *resp = s_http_json_work;
|
||||
memset(resp, 0, HTTP_JSON_WORK_CAP);
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
|
||||
int len = snprintf(resp, SYSTEM_STATUS_JSON_CAP,
|
||||
/* ── 分块 1:核心状态 + OTA/WiFi ── */
|
||||
int len = snprintf(resp, HTTP_JSON_WORK_CAP,
|
||||
"{\"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, \"channel\": %d, \"channel_name\": \"%s\", \"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, "
|
||||
"\"ota\": {\"sys_state\": \"%s\", \"sys_state_code\": %d, \"channel\": %d, \"channel_name\": \"%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\"}, ",
|
||||
FW_VERSION, FW_BUILD_TIME, FW_GIT_HASH, HealthMonitor::instance().getCurrent(),
|
||||
@@ -1284,9 +1414,6 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
(int)snap.sys_state,
|
||||
(int)snap.channel,
|
||||
snap.channel == OTA_CHANNEL_BLE ? "BLE" : "WIFI",
|
||||
(unsigned long)snap.meta.total_size,
|
||||
(unsigned long)snap.meta.stored_size,
|
||||
snap.storage_done ? "true" : "false",
|
||||
snap.relay.state,
|
||||
(unsigned long)snap.relay.seq,
|
||||
(unsigned long)snap.relay.total_packets,
|
||||
@@ -1295,11 +1422,30 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
snap.relay.version.app_patch, snap.relay.version.app_build,
|
||||
snap.relay.version.boot_major, snap.relay.version.boot_minor,
|
||||
snap.relay.version.boot_patch, snap.relay.version.boot_build);
|
||||
if (len < 0 || (size_t)len >= HTTP_JSON_WORK_CAP) {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "status hdr");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
len = append_cpu_status_json(resp, SYSTEM_STATUS_JSON_CAP, len, *cpu);
|
||||
/* ── 分块 2:CPU ── */
|
||||
len = append_cpu_status_json(resp, HTTP_JSON_WORK_CAP, 0, *cpu);
|
||||
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* SPI Link v2 统计(SPEC §12.1)— linkStatus 内部用 5ms 有界超时取 SPI 锁,
|
||||
* OTA 中继期间拿不到锁时返回零值统计,绝不阻塞 /system/status 响应。 */
|
||||
/* ── 分块 3:SPI / ADC / features / boot / memory ── */
|
||||
{
|
||||
SpiMaster::SpiLinkStatus ls = SpiMaster::instance().linkStatus();
|
||||
const char *role_str = "unknown";
|
||||
@@ -1308,7 +1454,7 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
case 2: role_str = "boot"; break;
|
||||
default: break;
|
||||
}
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
|
||||
len = append_jsonf(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,"
|
||||
@@ -1330,14 +1476,114 @@ esp_err_t system_status_handler(httpd_req_t *req)
|
||||
(unsigned long)ls.stats.max_resp_latency_us);
|
||||
}
|
||||
|
||||
/* 启动自检位图(与串口 BOOT report 同源) */
|
||||
len = SystemBoot::appendStatusJson(resp, SYSTEM_STATUS_JSON_CAP, len);
|
||||
{
|
||||
gd32_adc_feedback_t ga = {};
|
||||
const bool have = getLatestGd32AdcFeedback(&ga);
|
||||
const uint32_t age = gd32AdcFeedbackAgeMs();
|
||||
const bool fresh = have && age != UINT32_MAX && age < 5000U;
|
||||
const bool feat_known = SpiMaster::instance().gd32FeatKnown();
|
||||
const bool feat_adc = SpiMaster::instance().gd32FeatAdcFeedback();
|
||||
const char *en = feat_known ? (feat_adc ? "true" : "false")
|
||||
: (have ? "true" : "null");
|
||||
if (have) {
|
||||
len = append_jsonf(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,"
|
||||
"\"uptime_sec\":%lu,\"age_ms\":%lu}",
|
||||
en, feat_known ? "true" : "false",
|
||||
fresh ? "true" : "false", ga.valid ? "true" : "false",
|
||||
(double)ga.temperature_c, (double)ga.vdda_v, (double)ga.vin_adc_v,
|
||||
(double)ga.vcc_v,
|
||||
(unsigned)ga.temp_raw, (unsigned)ga.vref_raw,
|
||||
(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,
|
||||
",\"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,
|
||||
",\"gd32_features\":{\"known\":%s,\"flags\":%lu,"
|
||||
"\"adc_feedback\":%s,\"uart_temp_print\":%s,"
|
||||
"\"spi_app_log\":%s,\"can_brs\":%s,"
|
||||
"\"can_ch0\":%s,\"can_ch1\":%s,\"can_ch2\":%s,\"can_ch_mask\":%u}",
|
||||
feat_known ? "true" : "false",
|
||||
(unsigned long)fl,
|
||||
(feat_known && (fl & V2_FEAT_ADC_FEEDBACK)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_UART_TEMP_PRINT)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_SPI_APP_LOG)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_CAN_BRS)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_CAN_CH0)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_CAN_CH1)) ? "true" : "false",
|
||||
(feat_known && (fl & V2_FEAT_CAN_CH2)) ? "true" : "false",
|
||||
feat_known ? (unsigned)((fl & V2_FEAT_CAN_CH_MASK) >> V2_FEAT_CAN_CH_SHIFT) : 7U);
|
||||
}
|
||||
|
||||
// 前导逗号与前一块分隔;之前因 append_cpu_status_json 尾随逗号叠加
|
||||
// 形成 "],," 导致前端 JSON.parse 报 "Expected double-quoted property name"
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len, ",\"task_stack\": {");
|
||||
len = SystemBoot::appendStatusJson(resp, HTTP_JSON_WORK_CAP, len);
|
||||
|
||||
/* memory 动态字段:每次轮询;layout 仅 ?layout=1 时附带(链接期常量)。 */
|
||||
if (want_mem_layout) {
|
||||
len = append_jsonf(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,"
|
||||
"\"heap_total\":%lu,\"dma_total\":%lu,\"rtc_total\":%lu,"
|
||||
"\"task_stack_total\":%lu,\"default_heap_total\":%lu},"
|
||||
"\"heap_used\":%lu,\"heap_free\":%lu,"
|
||||
"\"heap_min_free\":%lu,\"heap_largest_free\":%lu,"
|
||||
"\"alloc_blocks\":%lu,\"free_blocks\":%lu,"
|
||||
"\"dma_free\":%lu,\"rtc_free\":%lu,\"default_heap_free\":%lu}",
|
||||
(unsigned long)sram_total,
|
||||
(unsigned long)sram_soc,
|
||||
(unsigned long)static_reserved,
|
||||
(unsigned long)reserved_tail,
|
||||
(unsigned long)iram_bytes,
|
||||
(unsigned long)data_bytes,
|
||||
(unsigned long)bss_bytes,
|
||||
(unsigned long)internal_heap_total,
|
||||
(unsigned long)dma_total,
|
||||
(unsigned long)rtc_total,
|
||||
(unsigned long)task_stack_total_bytes,
|
||||
(unsigned long)default_heap_total,
|
||||
(unsigned long)internal_heap_used,
|
||||
(unsigned long)internal_info.total_free_bytes,
|
||||
(unsigned long)internal_info.minimum_free_bytes,
|
||||
(unsigned long)internal_info.largest_free_block,
|
||||
(unsigned long)internal_info.allocated_blocks,
|
||||
(unsigned long)internal_info.free_blocks,
|
||||
(unsigned long)dma_free,
|
||||
(unsigned long)rtc_free,
|
||||
(unsigned long)free_heap);
|
||||
} else {
|
||||
len = append_jsonf(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,"
|
||||
"\"dma_free\":%lu,\"rtc_free\":%lu,\"default_heap_free\":%lu}",
|
||||
(unsigned long)internal_heap_used,
|
||||
(unsigned long)internal_info.total_free_bytes,
|
||||
(unsigned long)internal_info.minimum_free_bytes,
|
||||
(unsigned long)internal_info.largest_free_block,
|
||||
(unsigned long)internal_info.allocated_blocks,
|
||||
(unsigned long)internal_info.free_blocks,
|
||||
(unsigned long)dma_free,
|
||||
(unsigned long)rtc_free,
|
||||
(unsigned long)free_heap);
|
||||
}
|
||||
|
||||
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* ── 分块 4:task_stack + 闭合 ── */
|
||||
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, 0, ",\"task_stack\": {");
|
||||
for (size_t i = 0; i < task_metrics_count; ++i) {
|
||||
len = append_jsonf(resp, SYSTEM_STATUS_JSON_CAP, len,
|
||||
len = append_jsonf(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,
|
||||
@@ -1345,13 +1591,17 @@ 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, SYSTEM_STATUS_JSON_CAP, len, "}}");
|
||||
len = append_jsonf(resp, HTTP_JSON_WORK_CAP, len, "}}");
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, resp, len > 0 ? len : 0);
|
||||
if (cpu_heap) {
|
||||
free(cpu_heap);
|
||||
if (!status_flush_chunk(req, resp, HTTP_JSON_WORK_CAP, &len)) {
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* 结束 chunked body */
|
||||
(void)httpd_resp_send_chunk(req, NULL, 0);
|
||||
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
@@ -1386,3 +1636,41 @@ esp_err_t system_probe_handler(httpd_req_t *req)
|
||||
httpd_resp_send(req, resp, len);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GET /system/heap_low_log — 设备唯一快照槽(0 或 1 条)
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 返回设备侧最新一条"最低空闲创新低"上下文。
|
||||
* 多条历史由网页 IndexedDB 累积;JSON 复用 s_http_json_work(无独立 .bss)。
|
||||
*/
|
||||
esp_err_t system_heap_low_log_handler(httpd_req_t *req)
|
||||
{
|
||||
http_set_cors_headers(req);
|
||||
|
||||
SemaphoreHandle_t mux = httpJsonWorkMutex();
|
||||
if (mux && xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) {
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, "{\"records\":[]}", HTTPD_RESP_USE_STRLEN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
char *resp = s_http_json_work;
|
||||
int len = HealthMonitor::instance().dumpHeapLowWaterJson(resp, HTTP_JSON_WORK_CAP, 0);
|
||||
if (len < 0) {
|
||||
len = 0;
|
||||
}
|
||||
if ((size_t)len >= HTTP_JSON_WORK_CAP) {
|
||||
len = (int)HTTP_JSON_WORK_CAP - 1;
|
||||
}
|
||||
resp[len] = '\0';
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
esp_err_t err = httpd_resp_send(req, resp, len);
|
||||
if (mux) {
|
||||
xSemaphoreGive(mux);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,13 +15,15 @@ esp_err_t restart_handler(httpd_req_t *req);
|
||||
|
||||
// Utility handlers
|
||||
esp_err_t logs_handler(httpd_req_t *req);
|
||||
esp_err_t can_send_handler(httpd_req_t *req);
|
||||
/** @brief 批量 CAN 发送:一次 HTTP 连续发多帧,减少帧间 HTTP 往返延迟。 */
|
||||
/** @brief CAN 发送(唯一入口):同通道 1~N 帧,单帧即 frames 长度为 1。 */
|
||||
esp_err_t can_send_batch_handler(httpd_req_t *req);
|
||||
esp_err_t spi_send_handler(httpd_req_t *req);
|
||||
esp_err_t system_status_handler(httpd_req_t *req);
|
||||
esp_err_t system_probe_handler(httpd_req_t *req);
|
||||
|
||||
/** @brief GET /system/heap_low_log — 返回设备侧最新 1 条创新低上下文。 */
|
||||
esp_err_t system_heap_low_log_handler(httpd_req_t *req);
|
||||
|
||||
// CAN 反馈快照(被动 0xC1)与接收模式
|
||||
esp_err_t can_feedback_handler(httpd_req_t *req);
|
||||
esp_err_t can_mode_handler(httpd_req_t *req);
|
||||
|
||||
+56
-27
@@ -397,9 +397,26 @@ esp_err_t WifiOta::httpServerStart()
|
||||
config.server_port = HTTP_SERVER_PORT;
|
||||
config.stack_size = HTTP_SERVER_STACK_SIZE;
|
||||
config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
|
||||
/* LWIP_MAX_SOCKETS=12;HTTP 内部约占 3 + UDP 日志 1;业务连接上限 7 */
|
||||
config.max_open_sockets = 7;
|
||||
/* 满连接时淘汰最久未用的空闲 socket */
|
||||
/* Socket 预算(无法“合并” httpd 内部 3 个 fd:listen + ctrl 对):
|
||||
*
|
||||
* LWIP 总池 CONFIG_LWIP_MAX_SOCKETS
|
||||
* ├─ httpd 内部固定 3
|
||||
* ├─ max_open_sockets = 业务并发(下面 want)
|
||||
* └─ 余量:UDP 调试日志 1 + 偶发其它
|
||||
*
|
||||
* 约束:max_open_sockets <= LWIP_MAX_SOCKETS - 3
|
||||
*
|
||||
* 业务并发压到 4(tools 单浏览器已足够):
|
||||
* 1× /system/status 轮询 + 1× /logs 轮询 + 1× 操作(OTA/CAN) + 1× 余量
|
||||
* OTA 大上传期间前端 pausePolling,只占 1 条长连接,不与轮询抢槽。
|
||||
* lru_purge:满槽时踢最久空闲连接,避免 EMFILE 卡死。
|
||||
* 硬编码 7 在 LWIP=8 时会 ESP_ERR_INVALID_ARG → HTTP 起不来。 */
|
||||
{
|
||||
const int lwip_cap = CONFIG_LWIP_MAX_SOCKETS - 3; // httpd 硬上限
|
||||
const int want = 4; // 合并并发需求后的业务槽位(原 7 偏松)
|
||||
config.max_open_sockets = (lwip_cap < want) ? ((lwip_cap > 1) ? lwip_cap : 1) : want;
|
||||
}
|
||||
/* 满连接时淘汰最久未用的空闲 socket(相当于“复用/腾挪”槽位) */
|
||||
config.lru_purge_enable = true;
|
||||
/* idle 收发超时(秒);进行中的 handler 连接不受此 idle 检测踢掉 */
|
||||
config.recv_wait_timeout = 10;
|
||||
@@ -437,7 +454,7 @@ esp_err_t WifiOta::httpServerStart()
|
||||
{"/logs", HTTP_GET, logs_handler},
|
||||
{"/system/status", HTTP_GET, system_status_handler},
|
||||
{"/system/probe", HTTP_GET, system_probe_handler},
|
||||
{"/can/send", HTTP_POST, can_send_handler},
|
||||
{"/system/heap_low_log", HTTP_GET, system_heap_low_log_handler},
|
||||
{"/can/send_batch", HTTP_POST, can_send_batch_handler},
|
||||
{"/can/feedback", HTTP_GET, can_feedback_handler},
|
||||
{"/can/mode", HTTP_GET, can_mode_handler},
|
||||
@@ -445,7 +462,10 @@ esp_err_t WifiOta::httpServerStart()
|
||||
{"/spi/send", HTTP_POST, spi_send_handler},
|
||||
};
|
||||
|
||||
for (const auto &r : kHttpRoutes) {
|
||||
/* 同一 path 可有多 method(如 GET+POST);OPTIONS 每个 path 只注册一次,
|
||||
* 否则 httpd 报 “handler ... method 6 already registered”。 */
|
||||
for (size_t i = 0; i < sizeof(kHttpRoutes) / sizeof(kHttpRoutes[0]); ++i) {
|
||||
const UriEntry &r = kHttpRoutes[i];
|
||||
httpd_uri_t main_uri = {};
|
||||
main_uri.uri = r.uri;
|
||||
main_uri.method = r.method;
|
||||
@@ -453,15 +473,24 @@ esp_err_t WifiOta::httpServerStart()
|
||||
main_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &main_uri);
|
||||
|
||||
httpd_uri_t opts_uri = {};
|
||||
opts_uri.uri = r.uri;
|
||||
opts_uri.method = HTTP_OPTIONS;
|
||||
opts_uri.handler = http_options_handler;
|
||||
opts_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
||||
bool need_opts = true;
|
||||
for (size_t j = 0; j < i; ++j) {
|
||||
if (strcmp(kHttpRoutes[j].uri, r.uri) == 0) {
|
||||
need_opts = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (need_opts) {
|
||||
httpd_uri_t opts_uri = {};
|
||||
opts_uri.uri = r.uri;
|
||||
opts_uri.method = HTTP_OPTIONS;
|
||||
opts_uri.handler = http_options_handler;
|
||||
opts_uri.user_ctx = NULL;
|
||||
httpd_register_uri_handler(WifiOta::instance().m_httpServer, &opts_uri);
|
||||
}
|
||||
}
|
||||
|
||||
DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /restart, /logs, /system/status, /system/probe, /can/send, /can/send_batch, /spi/send, /can/feedback, /can/mode");
|
||||
DEBUG_INFO("WIFI", "HTTP端点已注册 (ota/can/spi/logs/status)");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -562,7 +591,7 @@ bool WifiOta::earlyInit()
|
||||
DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
if (nvs_erased) {
|
||||
DEBUG_WARN("WIFI", "NVS 已擦除重建,WiFi PHY 校准数据已清除,将重新校准");
|
||||
DEBUG_WARN("WIFI", "NVS重建,PHY校准已清,将重校");
|
||||
}
|
||||
DEBUG_INFO("WIFI", "NVS 初始化成功");
|
||||
}
|
||||
@@ -721,7 +750,7 @@ void WifiOta::taskEntry(void *pvParameters)
|
||||
ota_meta_t boot_meta;
|
||||
if (OtaCoordinator::instance().loadGd32Meta(&boot_meta) == ESP_OK &&
|
||||
boot_meta.total_size > 0 && boot_meta.stored_size < boot_meta.total_size) {
|
||||
DEBUG_WARN("WIFI", "检测到不完整 OTA 缓存 (stored=%u total=%u),清除残留状态",
|
||||
DEBUG_WARN("WIFI", "不完整OTA缓存 stored=%u total=%u,清残留",
|
||||
(unsigned)boot_meta.stored_size, (unsigned)boot_meta.total_size);
|
||||
OtaCoordinator::instance().clearGd32Meta();
|
||||
OtaCoordinator::instance().abortCurrent();
|
||||
@@ -742,21 +771,21 @@ void WifiOta::taskEntry(void *pvParameters)
|
||||
// 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
// 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
|
||||
// 启动摘要(单次;前面已 delay 等网络/ARP 稳定)
|
||||
esp_chip_info_t chip;
|
||||
esp_chip_info(&chip);
|
||||
for (int i = 0; i < 2; i++) {
|
||||
DEBUG_INFO("WIFI", "========================================");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
|
||||
DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
|
||||
DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
|
||||
DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
|
||||
DEBUG_INFO("WIFI", "========================================");
|
||||
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
|
||||
if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
DEBUG_INFO("WIFI", "========================================");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
|
||||
DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
|
||||
DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
|
||||
DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", WifiOta::getMode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
|
||||
DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
|
||||
DEBUG_INFO("WIFI", "========================================");
|
||||
DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
|
||||
|
||||
/* 启动日志 burst 已过:环 64→16 槽,释放大约 7.5KB 给业务堆 */
|
||||
DebugLog::instance().shrinkRingAfterBoot();
|
||||
|
||||
// WiFi 状态监控
|
||||
// 每秒一次循环:每 10 秒检查 RSSI
|
||||
|
||||
Reference in New Issue
Block a user