/* * http_handlers.cpp - HTTP 请求处理器实现 */ #include #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_http_server.h" #include "esp_wifi.h" #include "esp_netif.h" #include "esp_system.h" #include "esp_timer.h" #include "esp_ota_ops.h" #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" #include "http_handlers.h" #include "wifi_ota.h" #include "esp32_ota_core.h" #include "ota_coordinator.h" #include "ota_relay.h" #include "storage/gd32_flash_store.h" #include "health_monitor.h" #include "monitor/debug_log.h" #include "monitor/cpu_stats.h" #include "spi_master.h" #include "canfd_bridge.h" #include "monitor/can_feedback_task.h" #include "monitor/gd32_adc_feedback.h" #include "monitor/task_stack.h" #include "ble_ota.h" #include "system_boot.h" static size_t parseHexToBytes(const char *hex, uint8_t *out, size_t max_out) { size_t hex_len = strlen(hex); size_t n = 0; for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) { char byte_str[3] = { hex[i], hex[i + 1], 0 }; out[n++] = (uint8_t)strtol(byte_str, NULL, 16); } return n; } static void bytesToHexUpper(const uint8_t *in, size_t n, char *out) { for (size_t i = 0; i < n; ++i) { snprintf(out + i * 2, 3, "%02X", in[i]); } out[n * 2] = '\0'; } /** * @brief 统一 JSON 错误响应(避免前端 resp.json() 被 httpd 明文坑)。 * @param http_status 如 "400 Bad Request" / "403 Forbidden" / "500 Internal Server Error" * @param code 机器可读错误码(snake_case) * @param message 人类可读说明(勿含未转义双引号) */ static esp_err_t http_send_json_error(httpd_req_t *req, const char *http_status, const char *code, const char *message) { char body[320]; snprintf(body, sizeof(body), "{\"status\":\"error\",\"code\":\"%s\",\"message\":\"%s\"}", code ? code : "error", message ? message : "error"); httpd_resp_set_status(req, http_status ? http_status : "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, body, HTTPD_RESP_USE_STRLEN); return ESP_FAIL; } /** @brief OTA 收流:堆紧时让 LWIP 回收,再 httpd_req_recv。 */ static void otaUploadPaceIfHeapTight(void) { const uint32_t free_pre = esp_get_free_heap_size(); if (free_pre < 8192U) { (void)esp_task_wdt_reset(); vTaskDelay(pdMS_TO_TICKS(free_pre < 4096U ? 5U : 1U)); } } void http_set_cors_headers(httpd_req_t *req) { httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS"); httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type"); httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600"); } esp_err_t http_options_handler(httpd_req_t *req) { http_set_cors_headers(req); httpd_resp_set_status(req, "204 No Content"); httpd_resp_send(req, NULL, 0); return ESP_OK; } esp_err_t esp32_ota_update_handler(httpd_req_t *req) { http_set_cors_headers(req); Esp32OtaCore &core = Esp32OtaCore::instance(); if (!core.isActive()) { /* 常见场景:finalize 已成功并重启 → 新固件无 OTA 会话 → 迟到的 /ota/update。 */ return http_send_json_error(req, "400 Bad Request", "ota_not_started", "OTA not started"); } char frontend_sha_hex[65] = {0}; bool has_frontend_sha = false; bool verify_only = false; if (req->content_len > 0 && req->content_len < 256) { char buf[256] = {0}; int ret = httpd_req_recv(req, buf, req->content_len); if (ret > 0) { buf[ret] = '\0'; cJSON *root = cJSON_Parse(buf); if (root) { cJSON *sha_item = cJSON_GetObjectItem(root, "sha256"); if (sha_item && cJSON_IsString(sha_item)) { const char *sha_str = cJSON_GetStringValue(sha_item); if (sha_str && strlen(sha_str) == 64) { strncpy(frontend_sha_hex, sha_str, 64); frontend_sha_hex[64] = '\0'; has_frontend_sha = true; DEBUG_INFO("OTA", "Frontend SHA-256: %s", frontend_sha_hex); } } cJSON *verify_item = cJSON_GetObjectItem(root, "verify_only"); if (verify_item && cJSON_IsBool(verify_item)) { verify_only = cJSON_IsTrue(verify_item); } cJSON_Delete(root); } } } char device_sha_hex[65] = {0}; if (verify_only) { DEBUG_INFO("OTA", "ESP32 OTA verify-only mode, %u bytes", (unsigned)core.getReceived()); esp_err_t sha_err = core.computeDeviceSha(device_sha_hex); if (sha_err != ESP_OK) { DEBUG_ERR("OTA", "verify-only: SHA 计算失败: %s", esp_err_to_name(sha_err)); return http_send_json_error(req, "500 Internal Server Error", "sha_compute_failed", esp_err_to_name(sha_err)); } char ok_resp[256] = {0}; snprintf(ok_resp, sizeof(ok_resp), "{\"status\": \"ok\", \"message\": \"SHA-256 computed\", \"sha256\": \"%s\"}", device_sha_hex); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, ok_resp, strlen(ok_resp)); return ESP_OK; } DEBUG_INFO("OTA", "ESP32 OTA finalize, %u bytes...", (unsigned)core.getReceived()); Esp32FinalizeResult fr = OtaCoordinator::instance().finalizeForReboot( has_frontend_sha ? frontend_sha_hex : nullptr); if (fr.err == ESP_OK) { char resp[320] = {0}; snprintf(resp, sizeof(resp), "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\", " "\"sha256\": \"%s\"}", fr.device_sha_hex); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); DEBUG_INFO("OTA", "响应已发送,等待 TCP 刷新..."); vTaskDelay(pdMS_TO_TICKS(100)); OtaCoordinator::instance().restartEsp32(); return ESP_OK; } if (fr.err == ESP_ERR_INVALID_VERSION) { char err_resp[256] = {0}; snprintf(err_resp, sizeof(err_resp), "{\"status\": \"error\", \"message\": \"SHA-256 mismatch\", \"frontend\": \"%s\", \"device\": \"%s\"}", frontend_sha_hex, fr.device_sha_hex); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, err_resp, strlen(err_resp)); } else { return http_send_json_error(req, "500 Internal Server Error", "finalize_failed", esp_err_to_name(fr.err)); } return ESP_FAIL; } esp_err_t restart_handler(httpd_req_t *req) { (void)req; http_set_cors_headers(req); DEBUG_INFO("OTA", "收到重启请求,重启..."); const char resp[] = "{\"status\": \"ok\", \"message\": \"rebooting\"}"; httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); OtaCoordinator::instance().restartEsp32(); return ESP_OK; } static int recv_json_header(httpd_req_t *req, char *buf, int buf_size) { int total = 0; int depth = 1; while (total < buf_size - 1) { int ret = httpd_req_recv(req, buf + total, 1); if (ret <= 0) return total; total += ret; buf[total] = '\0'; if (buf[total - 1] == '{') depth++; else if (buf[total - 1] == '}') { depth--; if (depth == 0) return total; } } return total; } /* OTA 上传静态收块:避免 OTA 中途再 malloc(2KB) 与 TCP 窗口抢堆。 */ static uint8_t s_ota_upload_recv[OTA_BLOCK_SIZE]; static uint8_t s_ota_json_scratch[256]; static uint8_t s_ota_preread[256]; 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", "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)); DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len); esp_wifi_set_ps(WIFI_PS_NONE); // WiFi OTA 开始时恢复 BLE 共存状态(通过协调器) OtaCoordinator::instance().notifyBleCoexEnd(); char peek; int peek_ret = httpd_req_recv(req, &peek, 1); if (peek_ret <= 0) { return http_send_json_error(req, "500 Internal Server Error", "recv_failed", "receive failed"); } bool is_esp32_ota = false; uint32_t total_size = 0; int json_header_len = 0; /* 静态 scratch:OTA 期间少两次 256B heap 分配 */ uint8_t *pre_read_buf = s_ota_preread; memset(pre_read_buf, 0, 256); int pre_read_len = 1; pre_read_buf[0] = (uint8_t)peek; if (peek == '{') { char *json_buf = (char *)s_ota_json_scratch; memset(json_buf, 0, 256); json_buf[0] = peek; json_header_len = recv_json_header(req, json_buf + 1, 255); if (json_header_len > 0) { json_header_len++; json_buf[json_header_len] = '\0'; pre_read_len = json_header_len; if (pre_read_len > 256) pre_read_len = 256; memcpy(pre_read_buf, json_buf, pre_read_len); cJSON *root = cJSON_Parse(json_buf); if (root) { cJSON *type_item = cJSON_GetObjectItem(root, "type"); if (type_item && cJSON_IsString(type_item) && strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) { is_esp32_ota = true; } cJSON *size_item = cJSON_GetObjectItem(root, "total_size"); if (size_item) { total_size = (uint32_t)cJSON_GetNumberValue(size_item); } cJSON_Delete(root); } } } if (is_esp32_ota) { if (total_size == 0) { return http_send_json_error(req, "400 Bad Request", "missing_total_size", "missing total_size"); } /* ESP32 路径不再需要 pre_read(JSON 已消费,固件从流继续读) */ pre_read_len = 0; esp_err_t err = OtaCoordinator::instance().beginEsp32OtaWithRetry(total_size, OTA_CHANNEL_WIFI); if (err != ESP_OK) { if (err == ESP_ERR_INVALID_STATE) { return http_send_json_error(req, "403 Forbidden", "ota_occupied", "ESP32 OTA occupied by another channel"); } return http_send_json_error(req, "500 Internal Server Error", "ota_begin_failed", "OTA begin failed"); } Esp32OtaCore &core = Esp32OtaCore::instance(); uint8_t *fw_buf = recv_buf; // ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler // 若同类型重启已发生(新请求接管),旧 handler 的 abortCurrentIfEpoch 不会中止新会话 uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch(); size_t total_firmware_written = 0; size_t total_received = json_header_len; 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); /* 浏览器 upload 进度到 100% 只表示 TCP 发完;设备仍在 recv+Flash。 * sequential 擦写期间仍需让 LWIP 有机会跑;堆紧时 pacing。 */ const int64_t t_recv0 = esp_timer_get_time(); uint32_t write_chunks = 0U; while (total_received < (size_t)req->content_len) { size_t to_read = (size_t)req->content_len - total_received; if (to_read > OTA_BLOCK_SIZE) { to_read = OTA_BLOCK_SIZE; } otaUploadPaceIfHeapTight(); int ret = httpd_req_recv(req, (char *)fw_buf, to_read); if (ret <= 0) { 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); return http_send_json_error(req, "500 Internal Server Error", "http_recv_failed", "HTTP recv failed"); } err = core.write(fw_buf, (size_t)ret); if (err != ESP_OK) { DEBUG_ERR("WIFI", "OTA write failed: %s @%u free=%lu", esp_err_to_name(err), (unsigned)total_firmware_written, (unsigned long)esp_get_free_heap_size()); OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch); return http_send_json_error(req, "500 Internal Server Error", "ota_write_failed", "OTA write failed"); } total_firmware_written += (size_t)ret; total_received += (size_t)ret; ++write_chunks; (void)esp_task_wdt_reset(); } /* 回包前不打 INFO:堆紧时 DEBUG_INFO→UDP 会与 httpd_resp_send 抢 TX pbuf。 * 先让 LWIP 回收,成功后再记一条。 */ vTaskDelay(pdMS_TO_TICKS(10)); (void)esp_task_wdt_reset(); taskYIELD(); const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", " "\"target\": \"esp32\"}"; httpd_resp_set_type(req, "application/json"); 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", esp_err_to_name(send_err), (unsigned long)esp_get_free_heap_size()); /* 再试一次极简回包 */ send_err = httpd_resp_send(req, "{\"status\":\"ok\",\"target\":\"esp32\"}", HTTPD_RESP_USE_STRLEN); if (send_err != ESP_OK) { DEBUG_ERR("WIFI", "ESP32 OTA 简短响应仍失败: %s", esp_err_to_name(send_err)); } } else { 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()); } return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL; } const uint32_t gd32_fw_size = (uint32_t)req->content_len; uint32_t gd32_valid_size; if (json_header_len > 0 && (uint32_t)json_header_len < gd32_fw_size) { gd32_valid_size = (total_size > 0 && total_size < gd32_fw_size) ? total_size : (gd32_fw_size - (uint32_t)json_header_len); } else { gd32_valid_size = gd32_fw_size; } if (OtaRelay::getState() != RELAY_IDLE) { DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", OtaRelay::getState()); OtaRelay::instance().reset(); } // 与 BLE 共用 prepareGd32Storage(内含 validateGd32Size + begin + erase + meta) esp_err_t prep_err = OtaCoordinator::instance().prepareGd32Storage(gd32_valid_size, OTA_CHANNEL_WIFI); if (prep_err == ESP_ERR_INVALID_SIZE) { DEBUG_ERR("WIFI", "firmware size invalid: valid=%u max=%u", (unsigned)gd32_valid_size, (unsigned)OTA_MAX_FIRMWARE_SIZE); return http_send_json_error(req, "500 Internal Server Error", "firmware_too_large", "firmware too large or alignment overflow"); } else if (prep_err == ESP_ERR_INVALID_STATE) { DEBUG_ERR("WIFI", "GD32 OTA 协调器拒绝存储(系统忙)"); return http_send_json_error(req, "403 Forbidden", "system_busy", "system busy with another OTA operation"); } else if (prep_err != ESP_OK) { DEBUG_ERR("WIFI", "GD32 OTA 准备失败: %s", esp_err_to_name(prep_err)); return http_send_json_error(req, "500 Internal Server Error", "prepare_failed", esp_err_to_name(prep_err)); } // ★ 捕获当前 epoch,用于失败清理时检查会话是否仍属于本 handler uint32_t my_epoch = OtaCoordinator::instance().getOtaEpoch(); uint8_t *buf = recv_buf; int received = 0; // expected_fw_size_raw 仅用于后续日志;meta 已由 prepareGd32Storage 写入 uint32_t expected_fw_size_raw = gd32_valid_size; uint32_t gd32_offset = 0; DEBUG_INFO("WIFI", "GD32 raw upload start: %u bytes", expected_fw_size_raw); if (json_header_len == 0 && pre_read_len > 0) { int first_chunk_len = pre_read_len; if (first_chunk_len > req->content_len) first_chunk_len = req->content_len; if (first_chunk_len > 0) { gd32_offset = 0; esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(gd32_offset, pre_read_buf, (size_t)first_chunk_len); if (err != ESP_OK) { DEBUG_ERR("WIFI", "GD32 first chunk write failed: len=%d err=%s", first_chunk_len, esp_err_to_name(err)); OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch); return http_send_json_error(req, "500 Internal Server Error", "gd32_write_failed", "write gd32_ota failed"); } received = first_chunk_len; gd32_offset += (uint32_t)first_chunk_len; OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE)); } } else { received = pre_read_len; } while (received < req->content_len) { size_t to_read = req->content_len - received; if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE; otaUploadPaceIfHeapTight(); int ret = httpd_req_recv(req, (char *)buf, to_read); if (ret <= 0) { DEBUG_ERR("WIFI", "GD32 upload interrupted: received=%d expected=%d", received, req->content_len); OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch); return http_send_json_error(req, "500 Internal Server Error", "upload_interrupted", "upload interrupted"); } esp_err_t err = OtaCoordinator::instance().writeGd32Chunk(gd32_offset, buf, (size_t)ret); if (err != ESP_OK) { DEBUG_ERR("WIFI", "GD32 write failed: offset=%u len=%d err=%s", (unsigned)gd32_offset, ret, esp_err_to_name(err)); OtaCoordinator::instance().abortCurrentIfEpoch(my_epoch); return http_send_json_error(req, "500 Internal Server Error", "gd32_write_failed", "write gd32_ota failed"); } received += ret; gd32_offset += (uint32_t)ret; (void)esp_task_wdt_reset(); } OtaCoordinator::instance().updateGd32Meta(gd32_offset, (uint32_t)(gd32_offset / OTA_BLOCK_SIZE)); ota_meta_t final_meta; OtaCoordinator::instance().loadGd32Meta(&final_meta); if (final_meta.total_size > 0 && final_meta.stored_size >= final_meta.total_size) { /* 先让出再算 SHA;回包前不打 INFO,避免与 httpd_resp_send 抢堆 */ vTaskDelay(pdMS_TO_TICKS(10)); (void)esp_task_wdt_reset(); // 与 BLE 共用 finishGd32StorageAfterReceive:complete + SHA esp_err_t sha_err = OtaCoordinator::instance().finishGd32StorageAfterReceive(); if (sha_err != ESP_OK) { /* 存盘已完成,仍回 status=ok;relay 阶段会再比前端 SHA。 * 此处必须醒目告警,避免只当 INFO 被忽略。 */ DEBUG_WARN("WIFI", "GD32 SHA预算失败:%s (relay再校)", esp_err_to_name(sha_err)); } } const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\", " "\"message\": \"stored, call POST /ota/relay with sha256 to trigger\"}"; httpd_resp_set_type(req, "application/json"); 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", esp_err_to_name(send_err), (unsigned long)esp_get_free_heap_size()); } else { DEBUG_INFO("WIFI", "GD32 OTA 存储+响应完成: %u / %u free=%lu", (unsigned)gd32_offset, expected_fw_size_raw, (unsigned long)esp_get_free_heap_size()); } return (send_err == ESP_OK) ? ESP_OK : ESP_FAIL; } esp_err_t ota_relay_trigger_handler(httpd_req_t *req) { http_set_cors_headers(req); DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ==="); DEBUG_INFO("WIFI", "收到触发 OTA 中继请求"); ota_meta_t meta; memset(&meta, 0, sizeof(meta)); if (OtaCoordinator::instance().loadGd32Meta(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) { return http_send_json_error(req, "400 Bad Request", "firmware_not_stored", "firmware not fully stored"); } char frontend_sha_hex[65] = {0}; if (req->content_len > 0 && req->content_len < 512) { char buf[512] = {0}; int ret = httpd_req_recv(req, buf, req->content_len); if (ret > 0) { buf[ret] = '\0'; cJSON *root = cJSON_Parse(buf); if (root) { cJSON *sha_item = cJSON_GetObjectItem(root, "sha256"); if (sha_item && cJSON_IsString(sha_item) && strlen(cJSON_GetStringValue(sha_item)) == 64) { strncpy(frontend_sha_hex, cJSON_GetStringValue(sha_item), 64); frontend_sha_hex[64] = '\0'; } cJSON_Delete(root); } } } if (frontend_sha_hex[0] == '\0') { return http_send_json_error(req, "400 Bad Request", "missing_sha256", "missing sha256 in body (frontend SHA-256 required)"); } uint8_t frontend_sha[32] = {0}; for (int i = 0; i < 32; ++i) { char hex[3] = { frontend_sha_hex[i*2], frontend_sha_hex[i*2+1], 0 }; frontend_sha[i] = (uint8_t)strtoul(hex, nullptr, 16); } uint8_t device_sha[32] = {0}; char device_sha_hex[65] = {0}; esp_err_t verify_err = OtaCoordinator::instance().verifyGd32Firmware(frontend_sha, device_sha); for (int i = 0; i < 32; ++i) { snprintf(&device_sha_hex[i*2], 3, "%02X", device_sha[i]); } 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\", " "\"device\": \"%s\"}", device_sha_hex); httpd_resp_set_type(req, "application/json"); 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"); } char resp[256]; snprintf(resp, sizeof(resp), "{\"status\": \"ok\", \"message\": \"OTA relay triggered\", " "\"sha256\": \"%s\"}", device_sha_hex); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } esp_err_t logs_handler(httpd_req_t *req) { http_set_cors_headers(req); int json_len = 0; char *json = DebugLog::instance().ringDrainJson(&json_len); if (json && json_len > 0) { httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, json, json_len); free(json); } else { httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, "[]", 2); } return ESP_OK; } // ============================================================ // 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); can_feedback_result_t results[CAN_CHANNEL_MAX] = {}; bool valid[CAN_CHANNEL_MAX] = {}; for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { valid[i] = getLatestCanFeedback(i, &results[i]); } 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); } for (uint8_t i = 0; i < CAN_CHANNEL_MAX; ++i) { const can_feedback_result_t &r = results[i]; 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\":[", (unsigned)r.stats.mode, (unsigned long)r.stats.snapshot_seq, (unsigned long)r.stats.rx_count, (unsigned long)r.stats.tx_count, (unsigned long)r.stats.rx_drop_count, (unsigned long)r.stats.error_count, (unsigned)r.stats.tec, (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]; 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); if (elen < 0 || (size_t)elen >= sizeof(entry_tmp)) { break; } 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'; 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); } // ============================================================ // GET/POST /can/mode — 查询或切换三路 CAN 接收模式 // ============================================================ esp_err_t can_mode_handler(httpd_req_t *req) { http_set_cors_headers(req); if (req->method == HTTP_GET) { can_feedback_result_t fb{}; if (getLatestCanFeedback(0, &fb)) { char resp[64]; int len = snprintf(resp, sizeof(resp), "{\"mode\":%u}", (unsigned)fb.stats.mode); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); } else { can_rx_mode_t actual = CAN_RX_MODE_ALL_PASS; int rc = canFdSetRxMode(CAN_RX_MODE_ALL_PASS, &actual, 2000); char resp[64]; int len = snprintf(resp, sizeof(resp), "{\"mode\":%u,\"source\":\"spi\",\"ok\":%s}", (unsigned)actual, rc == 0 ? "true" : "false"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); } return ESP_OK; } char buf[128] = {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 *mode_item = cJSON_GetObjectItem(root, "mode"); if (!mode_item || !cJSON_IsNumber(mode_item)) { cJSON_Delete(root); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 mode 字段"); return ESP_FAIL; } int requested = (int)cJSON_GetNumberValue(mode_item); cJSON_Delete(root); can_rx_mode_t target = CAN_RX_MODE_ALL_PASS; if (requested == 1) { target = CAN_RX_MODE_FILTER_ID; } else if (requested == 2) { target = CAN_RX_MODE_BRIDGE_0_TO_12; } can_rx_mode_t actual = CAN_RX_MODE_ALL_PASS; int rc = canFdSetRxMode(target, &actual, 2000); char resp[96]; int len = snprintf(resp, sizeof(resp), "{\"ok\":%s,\"requested\":%d,\"actual\":%u}", rc == 0 ? "true" : "false", requested, (unsigned)actual); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, len); return ESP_OK; } /** * @brief POST /can/send_batch — CAN 发送唯一入口(单帧 = frames 长度 1)。 * * JSON: { "ch": 0, "frames": [ { "id": n, "dlc": n, "data": "HEX" }, ... ] } * 返回: { "status":"ok", "results":[ ... ], 可选 skipped/abort_* } * 单帧 canFdSend 超时 2000ms(等应答);多帧 80ms/帧 + 帧间隔。 * 发送期间暂停 CAN 反馈任务。 */ esp_err_t can_send_batch_handler(httpd_req_t *req) { http_set_cors_headers(req); constexpr int kMaxBody = 49152; constexpr int kMaxFrames = 128; constexpr uint32_t kPerFrameTimeoutMs = 80U; constexpr uint32_t kSingleFrameTimeoutMs = 2000U; constexpr uint32_t kInterFrameDelayMs = 5U; int content_len = req->content_len; if (content_len <= 0 || content_len > kMaxBody) { return http_send_json_error(req, "400 Bad Request", "bad_body", "body 长度无效或过大"); } char *buf = static_cast(malloc(static_cast(content_len) + 1U)); if (!buf) { return http_send_json_error(req, "500 Internal Server Error", "oom", "内存不足"); } int received = 0; while (received < content_len) { int ret = httpd_req_recv(req, buf + received, content_len - received); if (ret <= 0) { free(buf); return http_send_json_error(req, "400 Bad Request", "recv_fail", "接收数据失败"); } received += ret; } buf[received] = '\0'; cJSON *root = cJSON_Parse(buf); free(buf); buf = nullptr; if (!root) { return http_send_json_error(req, "400 Bad Request", "json_parse", "JSON 解析失败"); } cJSON *ch_item = cJSON_GetObjectItem(root, "ch"); cJSON *frames = cJSON_GetObjectItem(root, "frames"); if (!ch_item || !cJSON_IsArray(frames)) { cJSON_Delete(root); return http_send_json_error(req, "400 Bad Request", "bad_args", "需要 ch 与 frames 数组"); } can_channel_t ch = static_cast(cJSON_GetNumberValue(ch_item)); const int frame_count = cJSON_GetArraySize(frames); if (frame_count <= 0 || frame_count > kMaxFrames) { cJSON_Delete(root); return http_send_json_error(req, "400 Bad Request", "bad_frames", "frames 数量无效(1~128)"); } const uint32_t frame_timeout_ms = (frame_count == 1) ? kSingleFrameTimeoutMs : kPerFrameTimeoutMs; cJSON *out_root = cJSON_CreateObject(); cJSON *results = cJSON_CreateArray(); if (out_root == nullptr || results == nullptr) { cJSON_Delete(out_root); cJSON_Delete(results); cJSON_Delete(root); return http_send_json_error(req, "500 Internal Server Error", "oom", "JSON 分配失败"); } cJSON_AddStringToObject(out_root, "status", "ok"); cJSON_AddItemToObject(out_root, "results", results); /* 发送期间暂停 CAN 反馈,独占 SPI 业务路径 */ pauseCanFeedbackTask(true); auto fill_err = [](cJSON *item, int code, uint8_t gd32_err) { if (!item) { return; } cJSON_AddStringToObject(item, "status", "error"); cJSON_AddNumberToObject(item, "code", code); cJSON_AddNumberToObject(item, "gd32_err", gd32_err); }; for (int i = 0; i < frame_count; i++) { (void)esp_task_wdt_reset(); cJSON *item = cJSON_CreateObject(); if (item == nullptr) { break; } cJSON_AddItemToArray(results, item); cJSON *fr = cJSON_GetArrayItem(frames, i); if (!cJSON_IsObject(fr)) { fill_err(item, -1, 0); continue; } cJSON *id_item = cJSON_GetObjectItem(fr, "id"); cJSON *data_item = cJSON_GetObjectItem(fr, "data"); cJSON *dlc_item = cJSON_GetObjectItem(fr, "dlc"); if (!id_item) { fill_err(item, -1, 0); continue; } uint32_t ext_id = static_cast(cJSON_GetNumberValue(id_item)); uint8_t dlc = dlc_item ? static_cast(cJSON_GetNumberValue(dlc_item)) : 0U; if (dlc > CAN_MAX_DLC) { dlc = CAN_MAX_DLC; } uint8_t tx_data[CAN_MAX_DLC]; memset(tx_data, 0, sizeof(tx_data)); uint8_t tx_dlc = 0U; if (data_item && cJSON_IsString(data_item)) { const char *hex = cJSON_GetStringValue(data_item); if (hex) { tx_dlc = static_cast(parseHexToBytes(hex, tx_data, CAN_MAX_DLC)); } } if (tx_dlc == 0U && dlc > 0U) { tx_dlc = dlc; } uint8_t resp_data[CAN_MAX_DLC]; 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), frame_timeout_ms, &gd32_err); if (resp_len >= 0) { char resp_hex[CAN_MAX_DLC * 2 + 1]; memset(resp_hex, 0, sizeof(resp_hex)); const size_t copy_n = (static_cast(resp_len) < sizeof(resp_data)) ? static_cast(resp_len) : sizeof(resp_data); bytesToHexUpper(resp_data, copy_n, resp_hex); cJSON_AddStringToObject(item, "status", "ok"); cJSON_AddNumberToObject(item, "sent", tx_dlc); cJSON_AddNumberToObject(item, "recv_len", static_cast(copy_n)); cJSON_AddStringToObject(item, "recv", resp_hex); } else { fill_err(item, resp_len, gd32_err); /* SPI 失败 / BUS_OFF / TEC 过高:中止剩余帧,由 skipped 字段告知前端 */ const bool fatal_spi = (resp_len == -2); const bool fatal_bus = (resp_len == -3) && (gd32_err == 0xE2U || gd32_err == 0xE3U); if (fatal_spi || fatal_bus) { const int skipped = frame_count - i - 1; if (skipped > 0) { cJSON_AddNumberToObject(out_root, "skipped", skipped); cJSON_AddNumberToObject(out_root, "abort_code", resp_len); cJSON_AddNumberToObject(out_root, "abort_gd32_err", gd32_err); } break; } } if (i + 1 < frame_count) { vTaskDelay(pdMS_TO_TICKS(kInterFrameDelayMs)); } } pauseCanFeedbackTask(false); cJSON_Delete(root); root = nullptr; char *printed = cJSON_PrintUnformatted(out_root); cJSON_Delete(out_root); out_root = nullptr; if (!printed) { static const char kOom[] = "{\"status\":\"error\",\"code\":\"json_print\",\"message\":\"JSON输出失败\"}"; httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, kOom, HTTPD_RESP_USE_STRLEN); return ESP_FAIL; } httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, printed, HTTPD_RESP_USE_STRLEN); free(printed); return ESP_OK; } esp_err_t spi_send_handler(httpd_req_t *req) { http_set_cors_headers(req); char buf[512] = {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 *type_item = cJSON_GetObjectItem(root, "type"); cJSON *data_item = cJSON_GetObjectItem(root, "data"); cJSON *timeout_item = cJSON_GetObjectItem(root, "timeout_ms"); if (!type_item || !cJSON_IsString(type_item)) { cJSON_Delete(root); httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 type 字段"); return ESP_FAIL; } const char *type_hex = cJSON_GetStringValue(type_item); uint8_t frame_type = (uint8_t)strtol(type_hex, NULL, 16); uint32_t timeout_ms = timeout_item ? (uint32_t)cJSON_GetNumberValue(timeout_item) : 1000; uint8_t payload_data[SPI_OTA_FRAME_MAX] = {0}; size_t payload_len = 0; if (data_item && cJSON_IsString(data_item)) { payload_len = parseHexToBytes(cJSON_GetStringValue(data_item), payload_data, SPI_OTA_FRAME_MAX); } DEBUG_INFO("SPI", "WEB SPI: type=0x%02X, payload=%zu 字节", frame_type, payload_len); cJSON_Delete(root); char resp[1024]; uint8_t resp_type = 0; uint8_t resp_payload[SPI_OTA_FRAME_MAX] = {0}; size_t resp_len = 0; esp_err_t spi_err = SpiMaster::instance().transact(frame_type, payload_data, payload_len, &resp_type, resp_payload, sizeof(resp_payload), &resp_len, timeout_ms); if (spi_err == ESP_OK) { char resp_hex[SPI_OTA_FRAME_MAX * 2 + 1] = {0}; bytesToHexUpper(resp_payload, resp_len, resp_hex); snprintf(resp, sizeof(resp), "{\"status\": \"ok\", \"type\": \"%02X\", \"payload_len\": %zu, \"resp_type\": \"%02X\", \"resp_len\": %zu, \"recv\": \"%s\"}", frame_type, payload_len, resp_type, resp_len, resp_hex); /* 日志只记 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\"}", spi_err, esp_err_to_name(spi_err)); DEBUG_ERR("SPI", "WEB SPI 失败: %s", esp_err_to_name(spi_err)); } httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, resp, strlen(resp)); return ESP_OK; } static constexpr uint32_t INTERNAL_HEAP_CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; /** 发送当前工作区并清空 offset(分块;失败返回 false) */ static bool status_flush_chunk(httpd_req_t *req, char *buf, size_t cap, int *len) { if (!req || !buf || !len || *len <= 0) { return true; } 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; 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; } static int append_jsonf(char* buf, size_t cap, int offset, const char* fmt, ...) { if (!buf || cap == 0 || offset < 0 || (size_t)offset >= cap) return offset; va_list args; va_start(args, fmt); int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, args); va_end(args); if (n < 0) return offset; size_t written = (size_t)n; if (offset + (int)written >= (int)cap) { buf[cap - 1] = '\0'; return (int)(cap - 1); } return offset + (int)written; } /** 将 CpuStats 1Hz 快照写入 JSON(不在 HTTP 线程采样)。最多 16 条任务以免撑爆 3KB 分块。 */ static int append_cpu_status_json(char* buf, size_t cap, int offset, const CpuUsageSnapshot& cpu) { static constexpr size_t kCpuTaskJsonMax = 16; offset = append_jsonf(buf, cap, offset, "\"cpu\":{\"available\":%s,\"valid\":%s,\"usage_pct\":%.2f,\"idle_pct\":%.2f,\"sample_ms\":%lu,\"tasks\":[", cpu.available ? "true" : "false", cpu.valid ? "true" : "false", cpu.valid ? cpu.usage_pct : 0.0, cpu.valid ? cpu.idle_pct : 0.0, (unsigned long)(cpu.valid ? cpu.sample_ms : 0U)); 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}", i == 0 ? "" : ",", task.name, (unsigned long)task.task_number, task.runtime_delta, task.usage_pct); } // 不带尾随逗号;由后续拼接块自行添加前导逗号,避免双重逗号 return append_jsonf(buf, cap, offset, "]}"); } 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) { char tiny[192]; int n = snprintf(tiny, sizeof(tiny), "{\"version\":\"%s\",\"free_heap\":%lu,\"largest_free_block\":%lu," "\"wifi\":{\"connected\":%s},\"ota\":{\"sys_state\":\"degraded\"}," "\"cpu\":{\"available\":false,\"valid\":false}}", FW_VERSION, (unsigned long)esp_get_free_heap_size(), (unsigned long)largest_now, (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().getStaConnected()) ? "true" : "false"); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, tiny, n > 0 ? n : 0); return ESP_OK; } SemaphoreHandle_t mux = httpJsonWorkMutex(); if (mux) { if (xSemaphoreTake(mux, pdMS_TO_TICKS(200)) != pdTRUE) { /* 与 feedback/heap_low 争用工作区:仍回简包,避免 500 刷红 */ char busy[160]; snprintf(busy, sizeof(busy), "{\"version\":\"%s\",\"ota\":{\"sys_state\":\"busy\"}," "\"cpu\":{\"available\":false,\"valid\":false}}", FW_VERSION); httpd_resp_set_type(req, "application/json"); httpd_resp_send(req, busy, HTTPD_RESP_USE_STRLEN); return ESP_OK; } } char ip_str[16] = "0.0.0.0"; int8_t rssi = -127; bool connected = false; if (WifiOta::instance().m_wifiMode == OTA_WIFI_MODE_STA && WifiOta::instance().getStaConnected()) { esp_netif_ip_info_t ip_info; esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) { snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip)); } wifi_ap_record_t ap_info; if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { rssi = ap_info.rssi; } connected = true; } // 先自愈孤儿忙态(如 SHA 失败后卡在 ESP32_FINALIZING),再取快照给 UI OtaCoordinator::instance().healOrphanStates(); // 一次性快照,替代散读 OtaCoordinator / OtaRelay 多个 getter OtaSnapshot snap = OtaCoordinator::instance().getSnapshot(); 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); /* 内存全景:堆只是 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(a); const uintptr_t ub = reinterpret_cast(b); return (ub >= ua) ? static_cast(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(_heap_start); const size_t static_to_heap = (heap_start_addr >= static_cast(SOC_DRAM_LOW)) ? static_cast(heap_start_addr - static_cast(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) { task_metrics[i].handle = xTaskGetCurrentTaskHandle(); } } /* CPU:只读 cpu_stats 任务的 1Hz 快照,禁止在 httpd 里 uxTaskGetSystemState */ CpuUsageSnapshot *cpu = &s_cpu_snapshot; // 用静态缓冲,不再每秒 calloc(1.2KB) CpuStats::instance().getSnapshot(*cpu); char *resp = s_http_json_work; memset(resp, 0, HTTP_JSON_WORK_CAP); httpd_resp_set_type(req, "application/json"); /* ── 分块 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\", " "\"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(), (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", ip_str, rssi, connected ? "true" : "false", snap.sys_state_str, (int)snap.sys_state, (int)snap.channel, snap.channel == OTA_CHANNEL_BLE ? "BLE" : "WIFI", snap.relay.state, (unsigned long)snap.relay.seq, (unsigned long)snap.relay.total_packets, snap.relay.gd32_app_alive ? "true" : "false", snap.relay.version.app_major, snap.relay.version.app_minor, 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; } /* ── 分块 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; } /* ── 分块 3:SPI / ADC / features / boot / memory ── */ { SpiMaster::SpiLinkStatus ls = SpiMaster::instance().linkStatus(); const char *role_str = "unknown"; switch (ls.gd32_role) { case 1: role_str = "app"; break; case 2: role_str = "boot"; break; default: break; } 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," "\"retries\":%lu,\"ready_timeouts\":%lu,\"rx_overruns\":%lu," "\"resyncs\":%lu,\"max_resp_latency_us\":%lu}", (unsigned)ls.version, role_str, (unsigned long)ls.session_id, SpiMaster::instance().v2Active() ? "true" : "false", (unsigned)ls.local_credit, (unsigned)ls.remote_credit, (unsigned long)ls.stats.crc_errors, (unsigned long)ls.stats.duplicate_requests, (unsigned long)ls.stats.seq_errors, (unsigned long)ls.stats.retries, (unsigned long)ls.stats.ready_timeouts, (unsigned long)ls.stats.rx_overruns, (unsigned long)ls.stats.resyncs, (unsigned long)ls.stats.max_resp_latency_us); } { 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); } 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, 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, (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, HTTP_JSON_WORK_CAP, len, "}}"); 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); } return ESP_OK; } // ============================================================ // GET /system/probe — GD32 APP 单次 SPI 探测 // ============================================================ /** * @brief 显式触发一次 GD32 APP 存活探测。 * 成功时更新 OtaRelay 缓存的 alive/version 并返回版本信息。 */ esp_err_t system_probe_handler(httpd_req_t *req) { http_set_cors_headers(req); gd32_version_t ver = {}; uint32_t uptime = 0; esp_err_t err = OtaCoordinator::instance().probeGd32AppOnce(&ver, &uptime); char resp[256]; int len = snprintf(resp, sizeof(resp), "{\"alive\": %s, \"uptime_sec\": %" PRIu32 ", " "\"gd32_app\": \"v%d.%d.%d.%d\", \"gd32_boot\": \"v%d.%d.%d.%d\"}", (err == ESP_OK) ? "true" : "false", uptime, ver.app_major, ver.app_minor, ver.app_patch, ver.app_build, ver.boot_major, ver.boot_minor, ver.boot_patch, ver.boot_build); httpd_resp_set_type(req, "application/json"); 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; }