From 3160a33f502284f011bb25bac366927ed79d2045 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Thu, 8 Oct 2026 16:08:41 +0800 Subject: [PATCH] refactor(logging): remove remote logging subsystem --- 16m.csv | 1 - components/control_plane/CMakeLists.txt | 1 - .../internal/control_plane_internal.h | 7 - .../src/runtime/control_plane_factory_cli.c | 37 - .../src/runtime/control_plane_light_standby.c | 10 - .../control_plane_remote_log_service.c | 1363 ----------------- .../runtime/control_plane_runtime_workers.c | 7 - components/domain/include/domain.h | 1 - .../src/runtime/domain_runtime_adapters.c | 5 - components/platform/include/platform.h | 1 - .../platform/src/platform_behavior_stubs.c | 50 +- docs/architecture-and-flow-mermaid.md | 43 +- .../flash-and-memory-capacity-analysis.md | 2 + ...al-voice-startup-upload-isolation.zh-CN.md | 3 +- .../project-runtime-workflow.md | 5 +- ...diagnostics-and-network-recovery-config.md | 2 +- .../light-standby-power-optimization.zh-CN.md | 58 +- main/Kconfig.projbuild | 96 -- remote-logging-system-design.md | 539 ------- sdkconfig.defaults | 14 - tools/remote_log_receiver.py | 115 -- tools/view_flash_logs.command | 198 --- 22 files changed, 12 insertions(+), 2546 deletions(-) delete mode 100644 components/control_plane/src/runtime/control_plane_remote_log_service.c delete mode 100644 remote-logging-system-design.md delete mode 100644 tools/remote_log_receiver.py delete mode 100755 tools/view_flash_logs.command diff --git a/16m.csv b/16m.csv index c07399d..92d8777 100644 --- a/16m.csv +++ b/16m.csv @@ -4,7 +4,6 @@ nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, gifpack, data, littlefs, , 0x200000, -logfs, data, littlefs, , 0x40000, ota_0, app, ota_0, , 0x380000, ota_1, app, ota_1, , 0x380000, model, data, spiffs, , 0x300000, diff --git a/components/control_plane/CMakeLists.txt b/components/control_plane/CMakeLists.txt index 66907e3..f8cbc0f 100644 --- a/components/control_plane/CMakeLists.txt +++ b/components/control_plane/CMakeLists.txt @@ -17,7 +17,6 @@ idf_component_register( "src/runtime/control_plane_network_feedback.c" "src/runtime/control_plane_key_input.c" "src/runtime/control_plane_reset_service.c" - "src/runtime/control_plane_remote_log_service.c" "src/runtime/control_plane_runtime_workers.c" "src/runtime/control_plane_ota.c" "src/runtime/control_plane_factory_cli.c" diff --git a/components/control_plane/internal/control_plane_internal.h b/components/control_plane/internal/control_plane_internal.h index 6590d9e..defc92c 100644 --- a/components/control_plane/internal/control_plane_internal.h +++ b/components/control_plane/internal/control_plane_internal.h @@ -187,10 +187,3 @@ esp_err_t control_plane_schedule_websocket_wait_task(void); void control_plane_set_ptt_active(bool active); void control_plane_set_ptt_active_from_local_wake(void); esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason); - -esp_err_t control_plane_remote_log_start(void); -void control_plane_remote_log_stop(int timeout_ms); -void control_plane_remote_log_set_light_standby(bool standby); -esp_err_t control_plane_remote_log_flush_now(void); -esp_err_t control_plane_remote_log_clear(void); -esp_err_t control_plane_remote_log_format_status(char *out, size_t out_len); diff --git a/components/control_plane/src/runtime/control_plane_factory_cli.c b/components/control_plane/src/runtime/control_plane_factory_cli.c index 68b4419..148af5d 100644 --- a/components/control_plane/src/runtime/control_plane_factory_cli.c +++ b/components/control_plane/src/runtime/control_plane_factory_cli.c @@ -561,41 +561,6 @@ static void handle_prepare_rotate(uint32_t key_version) cli_write_ok(); } -static void handle_log_command(size_t argc, char *argv[]) -{ - if (argc < 2 || is_text_empty(argv[1])) { - cli_write_error("missing_log_command"); - return; - } - - if (strcmp(argv[1], "status") == 0) { - char json[FACTORY_CLI_JSON_MAX_LEN] = {0}; - esp_err_t err = control_plane_remote_log_format_status(json, sizeof(json)); - if (err != ESP_OK) { - cli_write_error("log_status_failed"); - return; - } - cli_write_line(json); - cli_write_ok(); - } else if (strcmp(argv[1], "flush") == 0) { - esp_err_t err = control_plane_remote_log_flush_now(); - if (err != ESP_OK) { - cli_write_error("log_flush_failed"); - return; - } - cli_write_ok(); - } else if (strcmp(argv[1], "clear") == 0) { - esp_err_t err = control_plane_remote_log_clear(); - if (err != ESP_OK) { - cli_write_error("log_clear_failed"); - return; - } - cli_write_ok(); - } else { - cli_write_error("unknown_log_command"); - } -} - static void handle_lcd_test(void) { const domain_expression_request_t request = { @@ -692,8 +657,6 @@ static void handle_command_line(char *line) return; } handle_prepare_rotate(key_version); - } else if (strcmp(cmd, "log") == 0) { - handle_log_command(argc, argv); } else if (strcmp(cmd, "lcd_test") == 0) { handle_lcd_test(); } else { diff --git a/components/control_plane/src/runtime/control_plane_light_standby.c b/components/control_plane/src/runtime/control_plane_light_standby.c index ccd738e..e0888f2 100644 --- a/components/control_plane/src/runtime/control_plane_light_standby.c +++ b/components/control_plane/src/runtime/control_plane_light_standby.c @@ -67,10 +67,6 @@ #define CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE 0 #endif -#ifndef CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD -#define CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD 0 -#endif - #ifndef CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC #define CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC 300 #endif @@ -214,9 +210,6 @@ static void control_plane_light_standby_unlock_transition(void) static esp_err_t control_plane_light_standby_apply_runtime_policy(bool standby) { -#if CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD - control_plane_remote_log_set_light_standby(standby); -#endif #if CONFIG_TQ_BATTERY_ENABLE control_plane_battery_service_set_light_standby(standby); #endif @@ -233,9 +226,6 @@ static esp_err_t control_plane_light_standby_apply_runtime_policy(bool standby) standby ? 1 : 0, esp_err_to_name(wifi_err)); if (standby) { -#if CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD - control_plane_remote_log_set_light_standby(false); -#endif #if CONFIG_TQ_BATTERY_ENABLE control_plane_battery_service_set_light_standby(false); #endif diff --git a/components/control_plane/src/runtime/control_plane_remote_log_service.c b/components/control_plane/src/runtime/control_plane_remote_log_service.c deleted file mode 100644 index 3e8d615..0000000 --- a/components/control_plane/src/runtime/control_plane_remote_log_service.c +++ /dev/null @@ -1,1363 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "control_plane_internal.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "domain.h" -#include "esp_app_desc.h" -#include "esp_core_dump.h" -#include "esp_heap_caps.h" -#include "esp_log.h" -#include "esp_system.h" -#include "esp_timer.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "sdkconfig.h" - -/* - * 远程日志采用“日志 hook -> 内存环形队列 -> JSONL 文件 -> 批量上传”的四段式所有权链。 - * hook 只复制有限长度文本,绝不联网或写文件;worker 独占文件轮转和上传,成功后才删除文件。 - * 本地控制台输出可保留,递归保护防止本模块自己的上传日志再次进入 hook。 - */ - -#ifndef CONFIG_TQ_REMOTE_LOG_ENABLE -#define CONFIG_TQ_REMOTE_LOG_ENABLE 0 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_ENDPOINT -#define CONFIG_TQ_REMOTE_LOG_ENDPOINT "" -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN -#define CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN "" -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_MIN_LEVEL -#define CONFIG_TQ_REMOTE_LOG_MIN_LEVEL 2 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT -#define CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT 1 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_QUEUE_LEN -#define CONFIG_TQ_REMOTE_LOG_QUEUE_LEN 128 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES -#define CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES 256 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES -#define CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES 30 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS -#define CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS 5000 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS -#define CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS 8000 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_MAX_FILES -#define CONFIG_TQ_REMOTE_LOG_MAX_FILES 32 -#endif - -#ifndef CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES -#define CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES 16384 -#endif - -#ifndef CONFIG_TQ_NETWORK_TASK_CORE -#define CONFIG_TQ_NETWORK_TASK_CORE 0 -#endif - -#ifndef CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS -#define CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS 5000 -#endif - -#define REMOTE_LOG_TASK_STACK_SIZE (1024 * 8) -#define REMOTE_LOG_TASK_PRIORITY 3 -#define REMOTE_LOG_DIR "/logfs/remote_logs" -#define REMOTE_LOG_FILE_PREFIX "log_" -#define REMOTE_LOG_FILE_SUFFIX ".jsonl" -#define REMOTE_LOG_PATH_MAX 96 -#define REMOTE_LOG_RESPONSE_MAX 256 -#define REMOTE_LOG_UPLOAD_BODY_MAX (CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES + 4096) -#define REMOTE_LOG_BACKOFF_MAX_MS 300000 -#define REMOTE_LOG_POLL_MS 200 -#define REMOTE_LOG_VOICE_STARTUP_GRACE_MS 60000 - -/* - * 队列和单条长度均有界,压力过大时记录 dropped_count 而非阻塞产生日志的实时任务。 - * 语音启动后留 60 秒宽限,避免日志 TLS/上传与首个会话争夺内部 RAM 和网络带宽。 - */ - -#if CONFIG_TQ_REMOTE_LOG_ENABLE - -typedef struct { - int64_t ts_ms; - char level; - bool truncated; - char text[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES]; -} remote_log_entry_t; - -static remote_log_entry_t *s_queue; -static size_t s_queue_read; -static size_t s_queue_count; -static uint32_t s_dropped_count; -static uint32_t s_file_sequence; -static TaskHandle_t s_remote_log_task; -static volatile bool s_stop_requested; -static bool s_light_standby; -static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED; -static vprintf_like_t s_original_vprintf; -static bool s_hook_installed; -static uint32_t s_successful_uploads; -static uint32_t s_failed_uploads; -static int s_last_http_status = -1; -static esp_err_t s_last_upload_err = ESP_ERR_NOT_FOUND; -static int64_t s_last_upload_ms; -static int64_t s_last_success_ms; -static bool s_coredump_present; -static bool s_coredump_reported; -static size_t s_coredump_addr; -static size_t s_coredump_size; - -static void *remote_log_psram_calloc(size_t count, size_t size) -{ - if (count == 0U || size == 0U) { - return NULL; - } - return heap_caps_calloc(count, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); -} - -static void *remote_log_psram_malloc(size_t size) -{ - if (size == 0U) { - return NULL; - } - return heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); -} - -static bool allocate_queue(void) -{ - if (s_queue != NULL) { - return true; - } - - s_queue = (remote_log_entry_t *)remote_log_psram_calloc( - CONFIG_TQ_REMOTE_LOG_QUEUE_LEN, - sizeof(*s_queue)); - if (s_queue != NULL) { - s_queue_read = 0U; - s_queue_count = 0U; - } - return s_queue != NULL; -} - -static void release_queue(void) -{ - if (s_queue == NULL) { - return; - } - - heap_caps_free(s_queue); - s_queue = NULL; - s_queue_read = 0U; - s_queue_count = 0U; -} - -static bool remote_log_path_active(void) -{ - return xTaskGetCurrentTaskHandle() == s_remote_log_task; -} - -static bool remote_log_light_standby(void) -{ - bool standby; - portENTER_CRITICAL(&s_lock); - standby = s_light_standby; - portEXIT_CRITICAL(&s_lock); - return standby; -} - -static void remote_log_clear_task_handle(void) -{ - portENTER_CRITICAL(&s_lock); - s_remote_log_task = NULL; - portEXIT_CRITICAL(&s_lock); -} - -static void remote_log_notify_task(void) -{ - portENTER_CRITICAL(&s_lock); - if (s_remote_log_task != NULL) { - xTaskNotifyGive(s_remote_log_task); - } - portEXIT_CRITICAL(&s_lock); -} - -static char log_level_from_line(const char *line) -{ - if (line == NULL || line[0] == '\0') { - return 'I'; - } - if ((line[0] == 'E' || line[0] == 'W' || line[0] == 'I' || - line[0] == 'D' || line[0] == 'V') && - line[1] == ' ') { - return line[0]; - } - return 'I'; -} - -static int log_level_rank(char level) -{ - switch (level) { - case 'E': - return 1; - case 'W': - return 2; - case 'I': - return 3; - case 'D': - case 'V': - return 4; - default: - return 3; - } -} - -static bool should_capture_level(char level) -{ - int min_level = CONFIG_TQ_REMOTE_LOG_MIN_LEVEL; - if (min_level <= 0) { - return false; - } - return log_level_rank(level) <= min_level; -} - -static void enqueue_line(const char *line, bool truncated) -{ - /* hook 上下文不等待空间:队列满时累计丢弃数,稍后作为合成记录写入,保留压力证据。 */ - if (line == NULL || line[0] == '\0' || s_queue == NULL) { - return; - } - - char level = log_level_from_line(line); - if (!should_capture_level(level)) { - return; - } - - portENTER_CRITICAL(&s_lock); - size_t write_index = (s_queue_read + s_queue_count) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN; - if (s_queue_count == CONFIG_TQ_REMOTE_LOG_QUEUE_LEN) { - s_queue_read = (s_queue_read + 1U) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN; - s_queue_count--; - s_dropped_count++; - } - - remote_log_entry_t *entry = &s_queue[write_index]; - entry->ts_ms = esp_timer_get_time() / 1000; - entry->level = level; - entry->truncated = truncated; - strlcpy(entry->text, line, sizeof(entry->text)); - s_queue_count++; - portEXIT_CRITICAL(&s_lock); -} - -static int remote_log_vprintf(const char *fmt, va_list args) -{ - int ret = 0; - -#if CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT - if (s_original_vprintf != NULL) { - va_list local_args; - va_copy(local_args, args); - ret = s_original_vprintf(fmt, local_args); - va_end(local_args); - } -#endif - - if (remote_log_path_active()) { - return ret; - } - - char line[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES]; - va_list copy_args; - va_copy(copy_args, args); - int written = vsnprintf(line, sizeof(line), fmt, copy_args); - va_end(copy_args); - if (written < 0) { - return ret; - } - - bool truncated = written >= (int)sizeof(line); - size_t len = strlen(line); - while (len > 0U && (line[len - 1U] == '\n' || line[len - 1U] == '\r')) { - line[--len] = '\0'; - } - enqueue_line(line, truncated); - return ret; -} - -static bool dequeue_entry(remote_log_entry_t *out_entry) -{ - if (out_entry == NULL || s_queue == NULL) { - return false; - } - - bool ok = false; - portENTER_CRITICAL(&s_lock); - if (s_queue_count > 0U) { - *out_entry = s_queue[s_queue_read]; - s_queue_read = (s_queue_read + 1U) % CONFIG_TQ_REMOTE_LOG_QUEUE_LEN; - s_queue_count--; - ok = true; - } - portEXIT_CRITICAL(&s_lock); - return ok; -} - -static size_t snapshot_queue_count(void) -{ - size_t count = 0U; - portENTER_CRITICAL(&s_lock); - count = s_queue_count; - portEXIT_CRITICAL(&s_lock); - return count; -} - -static uint32_t snapshot_and_clear_dropped_count(void) -{ - uint32_t dropped = 0; - portENTER_CRITICAL(&s_lock); - dropped = s_dropped_count; - s_dropped_count = 0; - portEXIT_CRITICAL(&s_lock); - return dropped; -} - -static void restore_dropped_count(uint32_t dropped) -{ - if (dropped == 0U) { - return; - } - portENTER_CRITICAL(&s_lock); - s_dropped_count += dropped; - portEXIT_CRITICAL(&s_lock); -} - -static void sanitize_value(char *text, const char *key) -{ - if (text == NULL || key == NULL || key[0] == '\0') { - return; - } - - size_t key_len = strlen(key); - char *cursor = text; - while ((cursor = strstr(cursor, key)) != NULL) { - char *value = cursor + key_len; - while (value[0] == ' ' || value[0] == ':' || value[0] == '=') { - value++; - } - if (value[0] == '\0') { - break; - } - char *end = value; - while (end[0] != '\0' && end[0] != ' ' && end[0] != ',' && - end[0] != '&' && end[0] != '"' && end[0] != '\'') { - end++; - } - const char *replacement = "***REDACTED***"; - size_t repl_len = strlen(replacement); - size_t tail_len = strlen(end); - if (repl_len <= (size_t)(end - value)) { - memcpy(value, replacement, repl_len); - memmove(value + repl_len, end, tail_len + 1U); - cursor = value + repl_len; - } else { - cursor = end; - } - } -} - -static void sanitize_log_line(char *text) -{ - /* 落盘前脱敏常见认证字段;这是最后一道保护,不替代调用方避免打印秘密。 */ - sanitize_value(text, "password"); - sanitize_value(text, "Password"); - sanitize_value(text, "pwd"); - sanitize_value(text, "token"); - sanitize_value(text, "Token"); - sanitize_value(text, "Bearer"); - sanitize_value(text, "secret"); - sanitize_value(text, "api_key"); -} - -static void json_append_escaped(char *out, size_t out_len, size_t *offset, const char *text) -{ - if (out == NULL || out_len == 0U || offset == NULL || *offset >= out_len) { - return; - } - - for (const char *p = text != NULL ? text : ""; p[0] != '\0' && *offset < out_len - 1U; ++p) { - unsigned char ch = (unsigned char)p[0]; - const char *escape = NULL; - switch (ch) { - case '\\': - escape = "\\\\"; - break; - case '"': - escape = "\\\""; - break; - case '\b': - escape = "\\b"; - break; - case '\f': - escape = "\\f"; - break; - case '\n': - escape = "\\n"; - break; - case '\r': - escape = "\\r"; - break; - case '\t': - escape = "\\t"; - break; - default: - break; - } - if (escape != NULL) { - int written = snprintf(out + *offset, out_len - *offset, "%s", escape); - if (written < 0) { - return; - } - *offset += (size_t)written; - } else if (ch < 0x20U) { - int written = snprintf(out + *offset, out_len - *offset, "\\u%04x", ch); - if (written < 0) { - return; - } - *offset += (size_t)written; - } else { - out[(*offset)++] = (char)ch; - out[*offset] = '\0'; - } - } -} - -static bool make_log_dir(void) -{ - struct stat st = {0}; - if (stat(REMOTE_LOG_DIR, &st) == 0 && S_ISDIR(st.st_mode)) { - return true; - } - return mkdir(REMOTE_LOG_DIR, 0755) == 0 || errno == EEXIST; -} - -static void make_log_path(uint32_t sequence, char *out, size_t out_len) -{ - snprintf(out, - out_len, - "%s/%s%08lu%s", - REMOTE_LOG_DIR, - REMOTE_LOG_FILE_PREFIX, - (unsigned long)sequence, - REMOTE_LOG_FILE_SUFFIX); -} - -static bool parse_log_filename(const char *name, uint32_t *out_sequence) -{ - if (name == NULL || out_sequence == NULL) { - return false; - } - size_t prefix_len = strlen(REMOTE_LOG_FILE_PREFIX); - size_t suffix_len = strlen(REMOTE_LOG_FILE_SUFFIX); - size_t name_len = strlen(name); - if (name_len <= prefix_len + suffix_len || - strncmp(name, REMOTE_LOG_FILE_PREFIX, prefix_len) != 0 || - strcmp(name + name_len - suffix_len, REMOTE_LOG_FILE_SUFFIX) != 0) { - return false; - } - char number[12] = {0}; - size_t number_len = name_len - prefix_len - suffix_len; - if (number_len >= sizeof(number)) { - return false; - } - memcpy(number, name + prefix_len, number_len); - char *end = NULL; - unsigned long value = strtoul(number, &end, 10); - if (end == NULL || end[0] != '\0') { - return false; - } - *out_sequence = (uint32_t)value; - return true; -} - -static uint32_t find_next_file_sequence(void) -{ - DIR *dir = opendir(REMOTE_LOG_DIR); - if (dir == NULL) { - return 1U; - } - - uint32_t max_sequence = 0U; - struct dirent *entry = NULL; - while ((entry = readdir(dir)) != NULL) { - uint32_t sequence = 0U; - if (parse_log_filename(entry->d_name, &sequence) && sequence > max_sequence) { - max_sequence = sequence; - } - } - closedir(dir); - return max_sequence + 1U; -} - -static size_t collect_files(uint32_t *out_sequences, size_t capacity) -{ - if (out_sequences == NULL || capacity == 0U) { - return 0U; - } - - DIR *dir = opendir(REMOTE_LOG_DIR); - if (dir == NULL) { - return 0U; - } - - size_t count = 0U; - struct dirent *entry = NULL; - while ((entry = readdir(dir)) != NULL) { - uint32_t sequence = 0U; - if (parse_log_filename(entry->d_name, &sequence)) { - if (count < capacity) { - out_sequences[count++] = sequence; - } - } - } - closedir(dir); - - for (size_t i = 0; i < count; ++i) { - for (size_t j = i + 1U; j < count; ++j) { - if (out_sequences[j] < out_sequences[i]) { - uint32_t tmp = out_sequences[i]; - out_sequences[i] = out_sequences[j]; - out_sequences[j] = tmp; - } - } - } - return count; -} - -static size_t count_pending_files(void) -{ - uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES + 8] = {0}; - return collect_files(sequences, sizeof(sequences) / sizeof(sequences[0])); -} - -static void trim_old_files(void) -{ - /* 超过文件上限时只删最旧已封存批次,当前追加文件和内存队列不受影响。 */ - uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES + 8] = {0}; - size_t count = collect_files(sequences, sizeof(sequences) / sizeof(sequences[0])); - while (count > CONFIG_TQ_REMOTE_LOG_MAX_FILES) { - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequences[0], path, sizeof(path)); - (void)unlink(path); - memmove(&sequences[0], &sequences[1], (count - 1U) * sizeof(sequences[0])); - count--; - } -} - -static esp_err_t append_entry_to_file(const remote_log_entry_t *entry) -{ - if (entry == NULL) { - return ESP_ERR_INVALID_ARG; - } - if (!make_log_dir()) { - return ESP_FAIL; - } - - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(s_file_sequence, path, sizeof(path)); - struct stat st = {0}; - if (stat(path, &st) == 0 && st.st_size >= CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES) { - s_file_sequence++; - make_log_path(s_file_sequence, path, sizeof(path)); - } - - FILE *file = fopen(path, "a"); - if (file == NULL) { - return ESP_FAIL; - } - - char text[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES]; - strlcpy(text, entry->text, sizeof(text)); - sanitize_log_line(text); - - char escaped[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES * 2]; - escaped[0] = '\0'; - size_t escaped_len = 0U; - json_append_escaped(escaped, sizeof(escaped), &escaped_len, text); - - int written = fprintf(file, - "{\"ts_ms\":%lld,\"level\":\"%c\",\"truncated\":%s,\"message\":\"%s\"}\n", - (long long)entry->ts_ms, - entry->level, - entry->truncated ? "true" : "false", - escaped); - int flush_err = fflush(file); - int fsync_err = fsync(fileno(file)); - int close_err = fclose(file); - if (written <= 0 || flush_err != 0 || fsync_err != 0 || close_err != 0) { - return ESP_FAIL; - } - - trim_old_files(); - return ESP_OK; -} - -static void drain_memory_queue_to_flash(void) -{ - /* 先从队列取得值拷贝再写 flash,文件 I/O 期间不占用队列临界区。 */ - remote_log_entry_t entry = {0}; - while (dequeue_entry(&entry)) { - if (append_entry_to_file(&entry) != ESP_OK) { - portENTER_CRITICAL(&s_lock); - s_dropped_count++; - portEXIT_CRITICAL(&s_lock); - break; - } - } -} - -static bool realtime_path_busy(void) -{ - /* 音频/OTA 等实时路径繁忙时仍允许落盘,但推迟 TLS 上传以减少带宽和堆竞争。 */ -#if CONFIG_TQ_LOCAL_WAKE_ENABLE - if (!control_plane_ota_voice_services_allowed() || - !control_plane_voice_turn_is_idle()) { - return true; - } -#endif -#if CONFIG_ENABLE_MULTIMODAL_INTERACTION - control_plane_multimodal_runtime_state_t state = {0}; - if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) { - return false; - } - return state.starting || - (state.connected && state.has_active_task && !state.server_listening) || - state.client_speech_active || - state.response_pending || - state.remote_responding || - state.request_to_speak_pending || - state.local_responding_started || - state.received_downstream_audio; -#else - return false; -#endif -} - -static bool voice_startup_upload_gate_open(TickType_t startup_tick) -{ -#if CONFIG_TQ_LOCAL_WAKE_ENABLE && CONFIG_ENABLE_MULTIMODAL_INTERACTION - control_plane_multimodal_runtime_state_t state = {0}; - if (control_plane_multimodal_get_runtime_state(&state) == ESP_OK && - state.connected && state.has_active_task && state.server_listening) { - return true; - } - - return (xTaskGetTickCount() - startup_tick) >= - pdMS_TO_TICKS(REMOTE_LOG_VOICE_STARTUP_GRACE_MS); -#else - (void)startup_tick; - return true; -#endif -} - -static const char *reset_reason_name(esp_reset_reason_t reason) -{ - switch (reason) { - case ESP_RST_POWERON: - return "poweron"; - case ESP_RST_EXT: - return "external"; - case ESP_RST_SW: - return "software"; - case ESP_RST_PANIC: - return "panic"; - case ESP_RST_INT_WDT: - return "interrupt_wdt"; - case ESP_RST_TASK_WDT: - return "task_wdt"; - case ESP_RST_WDT: - return "other_wdt"; - case ESP_RST_DEEPSLEEP: - return "deepsleep"; - case ESP_RST_BROWNOUT: - return "brownout"; - case ESP_RST_SDIO: - return "sdio"; - default: - return "unknown"; - } -} - -static esp_err_t append_device_state(char *body, size_t body_len, size_t *offset) -{ - if (body == NULL || offset == NULL || *offset >= body_len) { - return ESP_ERR_INVALID_ARG; - } - - const esp_app_desc_t *app = esp_app_get_description(); - esp_reset_reason_t reset_reason = esp_reset_reason(); - control_plane_multimodal_runtime_state_t mm = {0}; - bool have_mm = (control_plane_multimodal_get_runtime_state(&mm) == ESP_OK); - - int written = snprintf(body + *offset, - body_len - *offset, - ",\"device_state\":{" - "\"reset_reason\":\"%s\"," - "\"reset_reason_code\":%d," - "\"firmware_version\":\"", - reset_reason_name(reset_reason), - (int)reset_reason); - if (written < 0 || (size_t)written >= body_len - *offset) { - return ESP_ERR_INVALID_SIZE; - } - *offset += (size_t)written; - json_append_escaped(body, body_len, offset, app != NULL ? app->version : ""); - written = snprintf(body + *offset, - body_len - *offset, - "\",\"project_name\":\""); - if (written < 0 || (size_t)written >= body_len - *offset) { - return ESP_ERR_INVALID_SIZE; - } - *offset += (size_t)written; - json_append_escaped(body, body_len, offset, app != NULL ? app->project_name : ""); - written = snprintf(body + *offset, - body_len - *offset, - "\",\"free_heap\":%lu," - "\"minimum_free_heap\":%lu," - "\"free_internal\":%lu," - "\"largest_internal\":%lu," - "\"free_spiram\":%lu," - "\"largest_spiram\":%lu," - "\"task_count\":%lu," - "\"multimodal_connected\":%s," - "\"multimodal_active_task\":%s," - "\"multimodal_client_speech_active\":%s," - "\"multimodal_remote_responding\":%s," - "\"coredump_present\":%s," - "\"coredump_size\":%lu" - "}", - (unsigned long)esp_get_free_heap_size(), - (unsigned long)esp_get_minimum_free_heap_size(), - (unsigned long)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), - (unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), - (unsigned long)heap_caps_get_free_size(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT), - (unsigned long)heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT), - (unsigned long)uxTaskGetNumberOfTasks(), - (have_mm && mm.connected) ? "true" : "false", - (have_mm && mm.has_active_task) ? "true" : "false", - (have_mm && mm.client_speech_active) ? "true" : "false", - (have_mm && mm.remote_responding) ? "true" : "false", - s_coredump_present ? "true" : "false", - (unsigned long)s_coredump_size); - if (written < 0 || (size_t)written >= body_len - *offset) { - return ESP_ERR_INVALID_SIZE; - } - *offset += (size_t)written; - return ESP_OK; -} - -static esp_err_t build_upload_body(uint32_t sequence, - uint32_t dropped_count, - char *body, - size_t body_len, - size_t *out_entry_count) -{ - if (body == NULL || body_len == 0U || out_entry_count == NULL) { - return ESP_ERR_INVALID_ARG; - } - *out_entry_count = 0U; - - char device_id[64] = {0}; - char serial_number[64] = {0}; - domain_runtime_read_device_identity(device_id, sizeof(device_id), serial_number, sizeof(serial_number)); - - size_t offset = 0U; - int written = snprintf(body, - body_len, - "{\"schema\":\"tq.remote_log.v1\",\"device_id\":\""); - if (written < 0 || (size_t)written >= body_len) { - return ESP_ERR_INVALID_SIZE; - } - offset = (size_t)written; - json_append_escaped(body, body_len, &offset, device_id); - written = snprintf(body + offset, - body_len - offset, - "\",\"serial_number\":\""); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - json_append_escaped(body, body_len, &offset, serial_number); - written = snprintf(body + offset, - body_len - offset, - "\",\"batch_id\":\"%lu\",\"sent_at_ms\":%lld,\"dropped_count\":%lu,\"entries\":[", - (unsigned long)sequence, - (long long)(esp_timer_get_time() / 1000), - (unsigned long)dropped_count); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequence, path, sizeof(path)); - FILE *file = fopen(path, "r"); - if (file == NULL) { - return ESP_ERR_NOT_FOUND; - } - - char line[CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES * 3]; - bool first = true; - while (fgets(line, sizeof(line), file) != NULL) { - size_t line_len = strlen(line); - while (line_len > 0U && (line[line_len - 1U] == '\n' || line[line_len - 1U] == '\r')) { - line[--line_len] = '\0'; - } - if (line_len == 0U) { - continue; - } - written = snprintf(body + offset, - body_len - offset, - "%s%s", - first ? "" : ",", - line); - if (written < 0 || (size_t)written >= body_len - offset) { - fclose(file); - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - first = false; - (*out_entry_count)++; - } - fclose(file); - - written = snprintf(body + offset, body_len - offset, "]"); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - esp_err_t state_err = append_device_state(body, body_len, &offset); - if (state_err != ESP_OK) { - return state_err; - } - written = snprintf(body + offset, body_len - offset, "}"); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - return (*out_entry_count > 0U || dropped_count > 0U) ? ESP_OK : ESP_ERR_NOT_FOUND; -} - -static bool upload_one_file(uint32_t sequence) -{ - /* 只有服务端明确接受后才删除源文件;传输或状态码失败均保留供下次重试。 */ - uint32_t dropped = snapshot_and_clear_dropped_count(); - char *body = (char *)remote_log_psram_malloc(REMOTE_LOG_UPLOAD_BODY_MAX); - char *response = (char *)remote_log_psram_malloc(REMOTE_LOG_RESPONSE_MAX); - if (body == NULL || response == NULL) { - free(body); - free(response); - restore_dropped_count(dropped); - return false; - } - - size_t entry_count = 0U; - esp_err_t err = build_upload_body(sequence, - dropped, - body, - REMOTE_LOG_UPLOAD_BODY_MAX, - &entry_count); - if (err != ESP_OK) { - free(body); - free(response); - restore_dropped_count(dropped); - if (err == ESP_ERR_NOT_FOUND) { - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequence, path, sizeof(path)); - (void)unlink(path); - } - return false; - } - - int status = -1; - size_t response_len = 0U; - err = domain_runtime_post_json(CONFIG_TQ_REMOTE_LOG_ENDPOINT, - body, - NULL, - CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN, - CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS, - 0, - 0, - response, - REMOTE_LOG_RESPONSE_MAX, - &response_len, - &status); - - free(body); - free(response); - s_last_upload_ms = esp_timer_get_time() / 1000; - s_last_upload_err = err; - s_last_http_status = status; - - if (err == ESP_OK && (status == 200 || status == 202 || status == 204)) { - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequence, path, sizeof(path)); - (void)unlink(path); - s_successful_uploads++; - s_last_success_ms = s_last_upload_ms; - return true; - } - - if (err == ESP_OK && status == 400) { - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequence, path, sizeof(path)); - (void)unlink(path); - restore_dropped_count(dropped); - s_failed_uploads++; - return true; - } - - restore_dropped_count(dropped); - s_failed_uploads++; - return false; -} - -static bool upload_pending_files(void) -{ - if (!control_plane_is_sta_connected() || realtime_path_busy()) { - return false; - } - - uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES] = {0}; - size_t count = collect_files(sequences, CONFIG_TQ_REMOTE_LOG_MAX_FILES); - if (count == 0U) { - return true; - } - return upload_one_file(sequences[0]); -} - -static void detect_coredump(void) -{ - size_t addr = 0U; - size_t size = 0U; - esp_err_t err = esp_core_dump_image_get(&addr, &size); - if (err == ESP_OK && size > 0U) { - s_coredump_present = true; - s_coredump_addr = addr; - s_coredump_size = size; - } else { - s_coredump_present = false; - s_coredump_addr = 0U; - s_coredump_size = 0U; - } -} - -static esp_err_t build_coredump_report_body(char *body, size_t body_len) -{ - if (body == NULL || body_len == 0U) { - return ESP_ERR_INVALID_ARG; - } - - char device_id[64] = {0}; - char serial_number[64] = {0}; - domain_runtime_read_device_identity(device_id, sizeof(device_id), serial_number, sizeof(serial_number)); - - char panic_reason[192] = {0}; -#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH && CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF - (void)esp_core_dump_get_panic_reason(panic_reason, sizeof(panic_reason)); -#endif - - size_t offset = 0U; - int written = snprintf(body, - body_len, - "{\"schema\":\"tq.coredump.report.v1\",\"device_id\":\""); - if (written < 0 || (size_t)written >= body_len) { - return ESP_ERR_INVALID_SIZE; - } - offset = (size_t)written; - json_append_escaped(body, body_len, &offset, device_id); - written = snprintf(body + offset, body_len - offset, "\",\"serial_number\":\""); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - json_append_escaped(body, body_len, &offset, serial_number); - written = snprintf(body + offset, - body_len - offset, - "\",\"sent_at_ms\":%lld,\"coredump_addr\":%lu,\"coredump_size\":%lu,\"panic_reason\":\"", - (long long)(esp_timer_get_time() / 1000), - (unsigned long)s_coredump_addr, - (unsigned long)s_coredump_size); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - json_append_escaped(body, body_len, &offset, panic_reason); - written = snprintf(body + offset, body_len - offset, "\""); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - offset += (size_t)written; - esp_err_t state_err = append_device_state(body, body_len, &offset); - if (state_err != ESP_OK) { - return state_err; - } - written = snprintf(body + offset, body_len - offset, "}"); - if (written < 0 || (size_t)written >= body_len - offset) { - return ESP_ERR_INVALID_SIZE; - } - return ESP_OK; -} - -static bool upload_coredump_report(void) -{ - if (!s_coredump_present || s_coredump_reported || - !control_plane_is_sta_connected() || realtime_path_busy()) { - return false; - } - - char *body = (char *)remote_log_psram_malloc(2048); - char *response = (char *)remote_log_psram_malloc(REMOTE_LOG_RESPONSE_MAX); - if (body == NULL || response == NULL) { - free(body); - free(response); - return false; - } - - esp_err_t err = build_coredump_report_body(body, 2048); - if (err != ESP_OK) { - free(body); - free(response); - return false; - } - - int status = -1; - size_t response_len = 0U; - err = domain_runtime_post_json(CONFIG_TQ_REMOTE_LOG_ENDPOINT, - body, - NULL, - CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN, - CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS, - 0, - 0, - response, - REMOTE_LOG_RESPONSE_MAX, - &response_len, - &status); - free(body); - free(response); - - s_last_upload_ms = esp_timer_get_time() / 1000; - s_last_upload_err = err; - s_last_http_status = status; - if (err == ESP_OK && (status == 200 || status == 202 || status == 204)) { - s_successful_uploads++; - s_last_success_ms = s_last_upload_ms; - s_coredump_reported = true; - return true; - } - s_failed_uploads++; - return false; -} - -static void log_startup_self_check(void) -{ - ESP_LOGW("remote_log", - "enabled endpoint=%s dir=%s pending_files=%u queue=%u queue_bytes=%u storage=PSRAM max_files=%u file_max=%u coredump_present=%d coredump_size=%u", - CONFIG_TQ_REMOTE_LOG_ENDPOINT, - REMOTE_LOG_DIR, - (unsigned)count_pending_files(), - (unsigned)CONFIG_TQ_REMOTE_LOG_QUEUE_LEN, - (unsigned)(CONFIG_TQ_REMOTE_LOG_QUEUE_LEN * sizeof(*s_queue)), - (unsigned)CONFIG_TQ_REMOTE_LOG_MAX_FILES, - (unsigned)CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES, - s_coredump_present, - (unsigned)s_coredump_size); -} - -static void install_hook(void) -{ - if (s_hook_installed) { - return; - } - s_original_vprintf = esp_log_set_vprintf(remote_log_vprintf); - s_hook_installed = true; -} - -static void uninstall_hook(void) -{ - if (!s_hook_installed) { - return; - } - (void)esp_log_set_vprintf(s_original_vprintf); - s_original_vprintf = NULL; - s_hook_installed = false; -} - -static void remote_log_task(void *arg) -{ - /* worker 独占 flash 文件和上传顺序:先排空内存,再按序上传,停止前尽力完成最后一次落盘。 */ - (void)arg; - - esp_err_t mount_err = domain_runtime_mount_logfs(); - if (mount_err != ESP_OK) { - release_queue(); - remote_log_clear_task_handle(); - vTaskDelete(NULL); - return; - } - (void)make_log_dir(); - s_file_sequence = find_next_file_sequence(); - detect_coredump(); - - install_hook(); - log_startup_self_check(); - - TickType_t last_flush = xTaskGetTickCount(); - TickType_t startup_tick = last_flush; - uint32_t backoff_ms = 0U; - TickType_t next_upload_tick = xTaskGetTickCount(); - - while (!s_stop_requested) { - TickType_t now = xTaskGetTickCount(); - bool light_standby = remote_log_light_standby(); - if ((now - last_flush) >= pdMS_TO_TICKS(CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS)) { - drain_memory_queue_to_flash(); - last_flush = now; - } - - if (!light_standby && - voice_startup_upload_gate_open(startup_tick) && - (backoff_ms == 0U || now >= next_upload_tick)) { - drain_memory_queue_to_flash(); - (void)upload_coredump_report(); - bool ok = upload_pending_files(); - if (ok) { - backoff_ms = 0U; - } else if (control_plane_is_sta_connected()) { - backoff_ms = backoff_ms == 0U ? 5000U : backoff_ms * 2U; - if (backoff_ms > REMOTE_LOG_BACKOFF_MAX_MS) { - backoff_ms = REMOTE_LOG_BACKOFF_MAX_MS; - } - next_upload_tick = xTaskGetTickCount() + pdMS_TO_TICKS(backoff_ms); - } - } - - uint32_t poll_ms = light_standby ? - (uint32_t)CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS : - REMOTE_LOG_POLL_MS; - (void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(poll_ms)); - } - - drain_memory_queue_to_flash(); - uninstall_hook(); - remote_log_clear_task_handle(); - vTaskDelete(NULL); -} - -esp_err_t control_plane_remote_log_start(void) -{ - if (CONFIG_TQ_REMOTE_LOG_ENDPOINT[0] == '\0') { - return ESP_ERR_INVALID_ARG; - } - if (s_remote_log_task != NULL) { - return ESP_OK; - } - - if (!allocate_queue()) { - ESP_LOGE("remote_log", - "PSRAM queue allocation failed: entries=%u entry_bytes=%u", - (unsigned)CONFIG_TQ_REMOTE_LOG_QUEUE_LEN, - (unsigned)sizeof(*s_queue)); - return ESP_ERR_NO_MEM; - } - - s_stop_requested = false; - portENTER_CRITICAL(&s_lock); - s_light_standby = false; - portEXIT_CRITICAL(&s_lock); - BaseType_t created = xTaskCreatePinnedToCore(remote_log_task, - "cp_remote_log", - REMOTE_LOG_TASK_STACK_SIZE, - NULL, - REMOTE_LOG_TASK_PRIORITY, - &s_remote_log_task, - CONFIG_TQ_NETWORK_TASK_CORE < 0 ? tskNO_AFFINITY : - (BaseType_t)CONFIG_TQ_NETWORK_TASK_CORE); - if (created != pdPASS) { - release_queue(); - return ESP_ERR_NO_MEM; - } - return ESP_OK; -} - -esp_err_t control_plane_remote_log_flush_now(void) -{ - if (s_remote_log_task == NULL) { - return ESP_ERR_INVALID_STATE; - } - drain_memory_queue_to_flash(); - if (!control_plane_is_sta_connected()) { - return ESP_ERR_INVALID_STATE; - } - (void)upload_coredump_report(); - return upload_pending_files() ? ESP_OK : ESP_FAIL; -} - -esp_err_t control_plane_remote_log_clear(void) -{ - /* 清理仅作用于远程日志目录和内存队列,不触碰系统日志分区或 coredump 本体。 */ - portENTER_CRITICAL(&s_lock); - s_queue_read = 0U; - s_queue_count = 0U; - s_dropped_count = 0U; - portEXIT_CRITICAL(&s_lock); - - uint32_t sequences[CONFIG_TQ_REMOTE_LOG_MAX_FILES + 8] = {0}; - size_t count = collect_files(sequences, sizeof(sequences) / sizeof(sequences[0])); - for (size_t i = 0; i < count; ++i) { - char path[REMOTE_LOG_PATH_MAX] = {0}; - make_log_path(sequences[i], path, sizeof(path)); - (void)unlink(path); - } - return ESP_OK; -} - -esp_err_t control_plane_remote_log_format_status(char *out, size_t out_len) -{ - if (out == NULL || out_len == 0U) { - return ESP_ERR_INVALID_ARG; - } - - uint32_t dropped = 0U; - portENTER_CRITICAL(&s_lock); - dropped = s_dropped_count; - portEXIT_CRITICAL(&s_lock); - - int written = snprintf(out, - out_len, - "{\"enabled\":true,\"running\":%s,\"hook_installed\":%s," - "\"endpoint\":\"%s\",\"dir\":\"%s\",\"queue_count\":%u," - "\"queue_capacity\":%u,\"pending_files\":%u," - "\"max_files\":%u,\"file_max_bytes\":%u," - "\"dropped_count\":%lu,\"successful_uploads\":%lu," - "\"failed_uploads\":%lu,\"last_http_status\":%d," - "\"last_upload_err\":%d,\"last_upload_ms\":%lld," - "\"last_success_ms\":%lld,\"coredump_present\":%s," - "\"coredump_reported\":%s,\"coredump_size\":%lu}", - s_remote_log_task != NULL ? "true" : "false", - s_hook_installed ? "true" : "false", - CONFIG_TQ_REMOTE_LOG_ENDPOINT, - REMOTE_LOG_DIR, - (unsigned)snapshot_queue_count(), - (unsigned)CONFIG_TQ_REMOTE_LOG_QUEUE_LEN, - (unsigned)count_pending_files(), - (unsigned)CONFIG_TQ_REMOTE_LOG_MAX_FILES, - (unsigned)CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES, - (unsigned long)dropped, - (unsigned long)s_successful_uploads, - (unsigned long)s_failed_uploads, - s_last_http_status, - (int)s_last_upload_err, - (long long)s_last_upload_ms, - (long long)s_last_success_ms, - s_coredump_present ? "true" : "false", - s_coredump_reported ? "true" : "false", - (unsigned long)s_coredump_size); - return (written > 0 && (size_t)written < out_len) ? ESP_OK : ESP_ERR_INVALID_SIZE; -} - -void control_plane_remote_log_stop(int timeout_ms) -{ - if (s_remote_log_task == NULL) { - release_queue(); - return; - } - - s_stop_requested = true; - remote_log_notify_task(); - TickType_t start = xTaskGetTickCount(); - TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_ms > 0 ? timeout_ms : 1); - while (s_remote_log_task != NULL && - (xTaskGetTickCount() - start) < timeout_ticks) { - vTaskDelay(pdMS_TO_TICKS(20)); - } - if (s_remote_log_task != NULL) { - uninstall_hook(); - vTaskDelete(s_remote_log_task); - s_remote_log_task = NULL; - } - release_queue(); -} - -void control_plane_remote_log_set_light_standby(bool standby) -{ - portENTER_CRITICAL(&s_lock); - s_light_standby = standby; - if (s_remote_log_task != NULL) { - xTaskNotifyGive(s_remote_log_task); - } - portEXIT_CRITICAL(&s_lock); -} -#else -esp_err_t control_plane_remote_log_start(void) -{ - return ESP_ERR_NOT_SUPPORTED; -} - -void control_plane_remote_log_stop(int timeout_ms) -{ - (void)timeout_ms; -} - -void control_plane_remote_log_set_light_standby(bool standby) -{ - (void)standby; -} - -esp_err_t control_plane_remote_log_flush_now(void) -{ - return ESP_ERR_NOT_SUPPORTED; -} - -esp_err_t control_plane_remote_log_clear(void) -{ - return ESP_ERR_NOT_SUPPORTED; -} - -esp_err_t control_plane_remote_log_format_status(char *out, size_t out_len) -{ - if (out == NULL || out_len == 0U) { - return ESP_ERR_INVALID_ARG; - } - int written = snprintf(out, out_len, "{\"enabled\":false}"); - return (written > 0 && (size_t)written < out_len) ? ESP_OK : ESP_ERR_INVALID_SIZE; -} -#endif diff --git a/components/control_plane/src/runtime/control_plane_runtime_workers.c b/components/control_plane/src/runtime/control_plane_runtime_workers.c index c9a7e29..39cd8cb 100644 --- a/components/control_plane/src/runtime/control_plane_runtime_workers.c +++ b/components/control_plane/src/runtime/control_plane_runtime_workers.c @@ -36,7 +36,6 @@ #define CONTROL_PLANE_TASK_MONITOR_TASK_STACK_SIZE (1024 * 4) #define CONTROL_PLANE_TASK_MONITOR_STACK_WARN_BYTES 1024U #define CONTROL_PLANE_TASK_MONITOR_FULL_REPORT_PERIOD 5U -#define CONTROL_PLANE_REMOTE_LOG_STOP_WAIT_MS 1500 #define CONTROL_PLANE_MODEL_CMD_QUEUE_LEN 4 #define CONTROL_PLANE_MODEL_CMD_RECV_TIMEOUT_MS 200 #define CONTROL_PLANE_TASK_STOP_WAIT_MS 1500 @@ -2750,11 +2749,6 @@ esp_err_t control_plane_start_runtime_workers(void) ESP_LOGI(TAG, "Local wake startup deferred for OTA HTTPS window"); } - esp_err_t remote_log_err = control_plane_remote_log_start(); - if (remote_log_err != ESP_OK && remote_log_err != ESP_ERR_NOT_SUPPORTED) { - ESP_LOGW(TAG, "Remote log start failed: %s", esp_err_to_name(remote_log_err)); - } - #if CONFIG_FORTUNE_ENABLE if (control_plane_get_task_handle(&s_daily_fortune_task) == NULL) { control_plane_request_fortune_refresh(); @@ -2808,7 +2802,6 @@ void control_plane_stop_event_observer_runtime(void) control_plane_set_runtime_stop_requested(true); control_plane_stop_key_input_task(); control_plane_stop_system_event_task(); - control_plane_remote_log_stop(CONTROL_PLANE_REMOTE_LOG_STOP_WAIT_MS); control_plane_set_runtime_stop_requested(false); } diff --git a/components/domain/include/domain.h b/components/domain/include/domain.h index c5db34f..1decb13 100644 --- a/components/domain/include/domain.h +++ b/components/domain/include/domain.h @@ -337,7 +337,6 @@ bool domain_runtime_slot_feature_switch_is_active(void); esp_err_t domain_runtime_prepare_storage(void); esp_err_t domain_runtime_mount_littlefs(void); -esp_err_t domain_runtime_mount_logfs(void); void domain_runtime_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, diff --git a/components/domain/src/runtime/domain_runtime_adapters.c b/components/domain/src/runtime/domain_runtime_adapters.c index df50945..065c740 100644 --- a/components/domain/src/runtime/domain_runtime_adapters.c +++ b/components/domain/src/runtime/domain_runtime_adapters.c @@ -910,11 +910,6 @@ esp_err_t domain_runtime_mount_littlefs(void) return platform_mount_littlefs(); } -esp_err_t domain_runtime_mount_logfs(void) -{ - return platform_mount_logfs(); -} - void domain_runtime_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, diff --git a/components/platform/include/platform.h b/components/platform/include/platform.h index 1a73b3d..cb8510d 100644 --- a/components/platform/include/platform.h +++ b/components/platform/include/platform.h @@ -222,7 +222,6 @@ esp_err_t platform_init(void); esp_err_t platform_prepare_storage(void); bool platform_storage_is_ready(void); esp_err_t platform_mount_littlefs(void); -esp_err_t platform_mount_logfs(void); void platform_read_device_identity(char *device_id, size_t device_id_len, char *serial_number, diff --git a/components/platform/src/platform_behavior_stubs.c b/components/platform/src/platform_behavior_stubs.c index 625a6d2..f2b0ac1 100644 --- a/components/platform/src/platform_behavior_stubs.c +++ b/components/platform/src/platform_behavior_stubs.c @@ -30,12 +30,10 @@ #error "CONFIG_TQ_POWER_HOLD_ACTIVE_LEVEL must be defined in Kconfig" #endif -/* 提示音和运行日志使用独立分区/挂载点,避免日志写入影响只读媒体资源。 */ +/* 提示音从 gifpack LittleFS 分区读取。 */ #define VOICE_PROMPT_DEFAULT_BASE_PATH "/littlefs/audio_prompts" #define VOICE_PROMPT_LITTLEFS_BASE_PATH "/littlefs" #define VOICE_PROMPT_LITTLEFS_PARTITION "gifpack" -#define PLATFORM_LOGFS_BASE_PATH "/logfs" -#define PLATFORM_LOGFS_PARTITION "logfs" /* * 提示音文件由4字节小端包长头和对应 raw-Opus payload 重复组成;单包上限2KB * 防止损坏文件触发异常分配。帧时长按60ms估算播放总时长,并额外留3秒用于队列 @@ -51,8 +49,6 @@ static const char *VOICE_TAG = "voice_prompt"; static bool s_power_hold_configured; static bool s_voice_prompt_littlefs_mount_attempted; static esp_err_t s_voice_prompt_littlefs_mount_result = ESP_ERR_INVALID_STATE; -static bool s_logfs_mount_attempted; -static esp_err_t s_logfs_mount_result = ESP_ERR_INVALID_STATE; /* GPIO 掩码集中构造,负数配置代表该板型不使用电源保持引脚。 */ static uint64_t power_manager_gpio_bit_mask(int gpio_num) @@ -168,50 +164,6 @@ esp_err_t platform_mount_littlefs(void) return err; } -esp_err_t platform_mount_logfs(void) -{ - /* 日志分区与提示音分区独立挂载,任一失败不会覆盖另一分区状态。 */ - if (s_logfs_mount_attempted) { - return s_logfs_mount_result; - } - - esp_vfs_littlefs_conf_t conf = { - .base_path = PLATFORM_LOGFS_BASE_PATH, - .partition_label = PLATFORM_LOGFS_PARTITION, - .partition = NULL, - .format_if_mount_failed = true, - .dont_mount = false, - }; - - esp_err_t err = esp_vfs_littlefs_register(&conf); - if (err == ESP_ERR_INVALID_STATE && - esp_littlefs_mounted(PLATFORM_LOGFS_PARTITION)) { - err = ESP_OK; - } - if (err == ESP_OK) { - size_t total_bytes = 0; - size_t used_bytes = 0; - esp_err_t info_err = esp_littlefs_info(PLATFORM_LOGFS_PARTITION, - &total_bytes, - &used_bytes); - if (info_err == ESP_OK) { - ESP_LOGI(VOICE_TAG, - "LogFS mounted at %s, used=%u total=%u", - PLATFORM_LOGFS_BASE_PATH, - (unsigned)used_bytes, - (unsigned)total_bytes); - } - } else { - ESP_LOGW(VOICE_TAG, - "Mount LogFS failed: %s", - esp_err_to_name(err)); - } - - s_logfs_mount_attempted = true; - s_logfs_mount_result = err; - return err; -} - esp_err_t voice_prompt_play_file(const char *path) { /* 文件采用“长度头 + Opus 数据”帧格式,逐帧送入统一的下行播放管线。 */ diff --git a/docs/architecture-and-flow-mermaid.md b/docs/architecture-and-flow-mermaid.md index 5d39514..28ce7da 100644 --- a/docs/architecture-and-flow-mermaid.md +++ b/docs/architecture-and-flow-mermaid.md @@ -37,7 +37,6 @@ flowchart LR Events["System event dispatch"] MultimodalPolicy["Multimodal session policy"] Fortune["Daily fortune policy"] - RemoteLog["Remote log service"] FactoryCli["Factory CLI diagnostics"] end @@ -53,7 +52,7 @@ flowchart LR LCD["ST7789 direct LCD backend"] Audio["ES8311 audio pipeline"] Network["Wi-Fi, BLE provisioning, HTTP, WebSocket"] - Storage["NVS, LittleFS, LogFS"] + Storage["NVS, LittleFS"] GPIO["GPIO, stepper, console UART"] end @@ -62,11 +61,9 @@ flowchart LR Wire --> Workers Connectivity --> RuntimeAdapters Workers --> Events - Workers --> RemoteLog MultimodalPolicy --> RuntimeAdapters Fortune --> FortuneDomain Events --> ExpressionMap - FactoryCli --> RemoteLog RuntimeAdapters --> LCD RuntimeAdapters --> Audio RuntimeAdapters --> Network @@ -123,7 +120,7 @@ sequenceDiagram Domain->>Platform: Wi-Fi and provisioning primitives App->>CP: control_plane_start_runtime_workers() - CP->>CP: start model, websocket wait, LCD, Wi-Fi state, events, keys, remote log, fortune + CP->>CP: start model, websocket wait, LCD, Wi-Fi state, events, keys, fortune ``` ## Runtime Worker Topology @@ -139,7 +136,6 @@ flowchart TB Start --> WifiTask["cp_wifi_state"] Start --> EventTask["cp_system_event"] Start --> KeyTask["cp_key_input"] - Start --> RemoteLogTask["cp_remote_log"] Start --> FortuneTask["cp_fortune\nif FORTUNE_ENABLE"] KeyTask --> EventTask @@ -148,7 +144,6 @@ flowchart TB Multimodal --> ModelTask Multimodal --> Audio["audio capture/playback"] WifiTask --> LcdTask - RemoteLogTask --> LogServer["remote log server"] FortuneTask --> FortuneApi["daily fortune API"] ``` @@ -259,33 +254,6 @@ sequenceDiagram end ``` -## Remote Logging Flow - -```mermaid -flowchart TD - EspLog["ESP_LOGx / printf log"] - Hook["esp_log_set_vprintf hook"] - Filter["filter by CONFIG_TQ_REMOTE_LOG_MIN_LEVEL"] - RamQueue["RAM queue\nCONFIG_TQ_REMOTE_LOG_QUEUE_LEN"] - FlushTimer["periodic flush\nCONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS"] - LogFS["/logfs/remote_logs\nLittleFS persistent queue"] - StaReady{"STA connected\nand realtime path idle?"} - Upload["HTTP POST JSON batch\nCONFIG_TQ_REMOTE_LOG_ENDPOINT"] - Server["remote_log_receiver.py\n/opt/logTools/logs"] - Backoff["exponential retry backoff"] - DropOld["trim old files\nCONFIG_TQ_REMOTE_LOG_MAX_FILES"] - CoreDump["detect flash coredump\nupload metadata report"] - - EspLog --> Hook --> Filter --> RamQueue - RamQueue --> FlushTimer --> LogFS --> DropOld - LogFS --> StaReady - CoreDump --> StaReady - StaReady -- yes --> Upload --> Server - StaReady -- no --> Backoff --> StaReady - Upload -- fail --> Backoff - Upload -- success --> Delete["delete uploaded local file"] -``` - ## Daily Fortune Flow ```mermaid @@ -314,15 +282,8 @@ flowchart LR UART["UART0 serial input"] CLI["factory CLI parser"] Identity["identity / auth commands"] - LogStatus["log status"] - LogFlush["log flush"] - LogClear["log clear"] - RemoteLog["control_plane_remote_log_*"] UARTOut["UART0 JSON/OK/ERROR output"] UART --> CLI CLI --> Identity --> UARTOut - CLI --> LogStatus --> RemoteLog --> UARTOut - CLI --> LogFlush --> RemoteLog --> UARTOut - CLI --> LogClear --> RemoteLog --> UARTOut ``` diff --git a/docs/knowledge vault/flash-and-memory-capacity-analysis.md b/docs/knowledge vault/flash-and-memory-capacity-analysis.md index 2fb572a..ad64441 100644 --- a/docs/knowledge vault/flash-and-memory-capacity-analysis.md +++ b/docs/knowledge vault/flash-and-memory-capacity-analysis.md @@ -1,5 +1,7 @@ # Flash 与内存容量分析、风险及后续开发建议 +> 历史容量快照:文中远程日志与 `logfs` 已从当前固件移除,分区地址和容量应以现行 `16m.csv` 与最新构建产物为准。 + 本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。 LittleFS 资源从源码到设备的详细流程见: diff --git a/docs/knowledge vault/multimodal-voice-startup-upload-isolation.zh-CN.md b/docs/knowledge vault/multimodal-voice-startup-upload-isolation.zh-CN.md index 82e328c..cbba550 100644 --- a/docs/knowledge vault/multimodal-voice-startup-upload-isolation.zh-CN.md +++ b/docs/knowledge vault/multimodal-voice-startup-upload-isolation.zh-CN.md @@ -1,5 +1,7 @@ # OTA 后首次语音启动与后台上传隔离修复说明 +> 历史实现记录:文中远程日志及 LogFS 上传路径已从当前固件移除,相关描述不代表现行行为。 + ## 1. 提交信息 ```text @@ -414,4 +416,3 @@ Listening 到达时间 4. 模拟服务端 task 启动超过 10 秒,确认出现 `Local wake timed-out task cleanup`,下一次唤醒创建新 task。 5. 在 ASR 结束到 TTS 开始期间观察远程日志,确认没有启动后台 HTTPS 上传。 6. 开机 60 秒后保持语音空闲,确认历史日志和 coredump 能继续上传,而不是永久被阻塞。 - diff --git a/docs/knowledge vault/project-runtime-workflow.md b/docs/knowledge vault/project-runtime-workflow.md index 163bfe2..35f881d 100644 --- a/docs/knowledge vault/project-runtime-workflow.md +++ b/docs/knowledge vault/project-runtime-workflow.md @@ -14,7 +14,7 @@ - Wi-Fi 连接、重连和配置热点; - 设备身份、JWT 和会话令牌; - 用户出生资料、每日运势和财富方向; -- NVS、LittleFS、远程日志和工厂 CLI。 +- NVS、LittleFS 和工厂 CLI。 ## 2. 分层架构 @@ -269,7 +269,6 @@ flowchart TB LCD["cp_lcd_render
LCD 启动"] Events["cp_system_event
事件分发"] Keys["cp_key_input
GPIO 与消抖"] - Logs["remote log worker"] Fortune["cp_fortune
每日运势"] Monitor["cp_task_monitor
可选诊断"] @@ -278,7 +277,6 @@ flowchart TB Start --> LCD Start --> Events Start --> Keys - Start --> Logs Start --> Fortune Start --> Monitor ``` @@ -705,4 +703,3 @@ control_plane_stop_runtime_workers() - `components/platform/src/multimodal/transport_ws/transport_ws_runtime_impl.c` - `components/platform/src/lcd_direct.c` - `components/platform/src/platform_stepper_driver.c` - diff --git a/docs/knowledge vault/runtime-diagnostics-and-network-recovery-config.md b/docs/knowledge vault/runtime-diagnostics-and-network-recovery-config.md index d664574..a25feb7 100644 --- a/docs/knowledge vault/runtime-diagnostics-and-network-recovery-config.md +++ b/docs/knowledge vault/runtime-diagnostics-and-network-recovery-config.md @@ -107,7 +107,7 @@ When enabled, the first multimodal HTTPS connection failure requests controlled When disabled, HTTPS and WebSocket retry behavior remains active, but the firmware does not intentionally disconnect a Wi-Fi connection that is still reported as associated. -Runtime impact when enabled: all device network traffic is interrupted during reassociation, including WebSocket, HTTPS, remote logging, LAN HTTP, and mDNS traffic. It is enabled by default because field testing demonstrated a stale STA data path that repeated transport-level reconnects could not recover. Disable it only when deliberate reassociation is unacceptable for a deployment. +Runtime impact when enabled: all device network traffic is interrupted during reassociation, including WebSocket, HTTPS, LAN HTTP, and mDNS traffic. It is enabled by default because field testing demonstrated a stale STA data path that repeated transport-level reconnects could not recover. Disable it only when deliberate reassociation is unacceptable for a deployment. The HTTPS preflight performs one application-level DNS lookup. lwIP already retransmits DNS queries internally, and the captured failure took about 14 diff --git a/docs/light-standby-power-optimization.zh-CN.md b/docs/light-standby-power-optimization.zh-CN.md index efb4e27..78825e8 100644 --- a/docs/light-standby-power-optimization.zh-CN.md +++ b/docs/light-standby-power-optimization.zh-CN.md @@ -9,7 +9,7 @@ | `988dc92` | 增加 LCD 背光 PWM 亮度控制 | | `6a11aca` | 增加轻待机状态协调器和活动事件接入 | | `baad9dc` | 增加背光、扬声器功放和电机的待机策略 | -| `e773ebc` | 增加 Wi-Fi、远程日志、电池检测和内存监控的待机策略 | +| `e773ebc` | 增加 Wi-Fi、电池检测和内存监控的待机策略(远程日志部分现已移除) | | `cb203a7` | 进入轻待机前播放提示音,并等待真实播放完成 | `bba99a0 fix(control-plane): trigger PTT on short press` 是独立的按键行为修复,不属于低功耗改动,因此不在本文范围内。 @@ -35,7 +35,6 @@ - 扬声器功放在无音频播放时仍可能保持供电。 - 步进电机完成动作后可能继续保持线圈电流。 - Wi-Fi 始终使用活跃策略。 -- 远程日志持续尝试 HTTP 上传。 - 电池检测和内存监控仍按活跃周期运行。 - 缺少统一的待机状态、空闲计时、唤醒入口和失败回滚机制。 - 如果进入待机时立即关闭功放,待机提示音将无法完整播放。 @@ -79,8 +78,6 @@ CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus" CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y -CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD=y -CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS=5000 CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC=300 CONFIG_TQ_LIGHT_STANDBY_MEMORY_SAMPLE_INTERVAL_MS=10000 CONFIG_TQ_LIGHT_STANDBY_MEMORY_REPORT_INTERVAL_MS=300000 @@ -372,51 +369,6 @@ esp_err_t platform_wifi_portal_set_light_standby(bool standby) - 可能增加网络首包延迟,实际幅度受 AP 的 DTIM、信号质量和网络负载影响。 - 不等同于关闭 Wi-Fi,网络任务和 WebSocket 仍会消耗资源。 -### 5.6 远程日志 - -#### 问题原因 - -待机期间继续发起 HTTP 上传会周期性唤醒网络和 CPU。完全停止日志采集又会丢失现场诊断信息。 - -#### 解决方式 - -待机期间继续接收日志并落盘,但暂停 coredump 和日志文件的 HTTP 上传;轮询间隔放宽至 5 秒。唤醒后恢复上传。 - -#### 具体代码 - -```c -while (!s_stop_requested) { - TickType_t now = xTaskGetTickCount(); - bool light_standby = remote_log_light_standby(); - - if ((now - last_flush) >= - pdMS_TO_TICKS(CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS)) { - drain_memory_queue_to_flash(); - last_flush = now; - } - - if (!light_standby && - voice_startup_upload_gate_open(startup_tick) && - (backoff_ms == 0U || now >= next_upload_tick)) { - drain_memory_queue_to_flash(); - upload_coredump_report(); - upload_pending_files(); - } - - uint32_t poll_ms = light_standby ? - CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS : - REMOTE_LOG_POLL_MS; - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(poll_ms)); -} -``` - -#### 功耗与影响 - -- 减少待机期间 HTTP、TLS/TCP 和 Wi-Fi 活动。 -- 日志仍会写入 Flash,不会因为轻待机直接丢失。 -- 待机期间服务端无法立即看到新日志;唤醒后再补传。 -- 持续落盘仍有 Flash 写入功耗和寿命影响,但保留了诊断能力。 - ### 5.7 电池检测 #### 问题原因 @@ -565,7 +517,6 @@ static esp_err_t control_plane_light_standby_apply_entry_policy(void) ```c static esp_err_t control_plane_light_standby_apply_runtime_policy(bool standby) { - control_plane_remote_log_set_light_standby(standby); control_plane_battery_service_set_light_standby(standby); control_plane_runtime_diagnostics_set_light_standby(standby); @@ -573,7 +524,6 @@ static esp_err_t control_plane_light_standby_apply_runtime_policy(bool standby) esp_err_t wifi_err = domain_runtime_wifi_portal_set_light_standby(standby); if (wifi_err != ESP_OK && standby) { - control_plane_remote_log_set_light_standby(false); control_plane_battery_service_set_light_standby(false); control_plane_runtime_diagnostics_set_light_standby(false); return wifi_err; @@ -643,11 +593,10 @@ esp_err_t control_plane_light_standby_record_activity( 4. 待机时 Codec 输出静音并关闭扬声器 PA,同时保留麦克风、AFE 和唤醒词。 5. 释放步进电机线圈保持电流。 6. Wi-Fi 切换为最小 Modem 省电,同时保留连接。 -7. 暂停远程日志 HTTP 上传,日志采集和落盘继续工作。 8. 电池常规采样周期放宽到 300 秒,充电状态边沿仍及时处理。 9. 内存监控采样放宽到 10 秒、周期报告放宽到 300 秒,分配失败仍及时处理。 10. 支持唤醒词、按键、SW2 抽签、模型命令和语音活动快速唤醒。 -11. 唤醒后恢复正常背光、功放、Wi-Fi、日志、电池和内存监控策略。 +11. 唤醒后恢复正常背光、功放、Wi-Fi、电池和内存监控策略。 12. 增加进入失败回滚和状态竞态保护。 13. 将轻待机任务栈增至 4096 字节,解决已观察到的任务栈溢出。 @@ -682,7 +631,6 @@ esp_err_t control_plane_light_standby_record_activity( | 麦克风 | 保持运行,以满足唤醒词快速唤醒要求 | | 电机 | 待机不保持静态扭矩,机构可能被外力移动 | | Wi-Fi | 保持连接,但首包延迟可能增加 | -| 远程日志 | 继续采集和落盘,服务端接收会延迟到唤醒后 | | 电池检测 | 普通电量更新变慢,充电状态边沿仍及时检测 | | 内存监控 | 普通水位报告变慢,分配失败仍及时处理 | | 提示音 | 真正进入待机的时间会比 5 分钟多出提示音播放时间 | @@ -702,6 +650,6 @@ esp_err_t control_plane_light_standby_record_activity( ## 10. 结论 -本次改动已经完成当前产品约束下的主要轻待机优化:保留基础表情、唤醒词和按键响应,同时降低背光、功放、电机、Wi-Fi、远程日志、电池检测和内存监控的持续功耗。实现具备统一状态管理、真实音频完成等待、进入失败回滚和快速唤醒恢复。 +本次改动已经完成当前产品约束下的主要轻待机优化:保留基础表情、唤醒词和按键响应,同时降低背光、功放、电机、Wi-Fi、电池检测和内存监控的持续功耗。实现具备统一状态管理、真实音频完成等待、进入失败回滚和快速唤醒恢复。 下一步重点不是继续盲目关闭模块,而是进行真机电流和唤醒延迟测试。测试数据可以确认主要耗电来源,并判断 LCD 刷新和音频下行链路是否值得进入下一轮优化。 diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 2cd18e6..b1aca84 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -439,20 +439,6 @@ menu "TQ Cat Configuration" Keep the STA and WebSocket connected while enabling minimum modem sleep. Active mode restores the configured Wi-Fi policy. - config TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD - bool "Pause remote log uploads in light standby" - default y - depends on TQ_LIGHT_STANDBY_ENABLE && TQ_REMOTE_LOG_ENABLE - help - Continue capturing and persisting logs, but defer HTTP - uploads until activity wakes the device. - - config TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS - int "Remote log standby poll interval (ms)" - default 5000 - range 1000 60000 - depends on TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD - config TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC int "Battery standby sample interval (seconds)" default 300 @@ -487,88 +473,6 @@ menu "TQ Cat Configuration" endmenu - menu "Remote Logging" - - config TQ_REMOTE_LOG_ENABLE - bool "Enable remote log upload" - default n - help - Capture ESP-IDF logs, persist them to the LittleFS-backed - queue, and upload batches to the configured HTTPS server. - - config TQ_REMOTE_LOG_ENDPOINT - string "Remote log endpoint URL" - default "https://gz.api.talkingq.com/log/batch" - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_AUTH_TOKEN - string "Remote log bearer token" - default "" - depends on TQ_REMOTE_LOG_ENABLE - help - Optional bearer token sent as Authorization: Bearer - . Leave empty for unauthenticated test receivers. - - config TQ_REMOTE_LOG_MIN_LEVEL - int "Minimum remote log level" - default 2 - range 0 4 - depends on TQ_REMOTE_LOG_ENABLE - help - 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG. - - config TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT - bool "Preserve existing local log output" - default y - depends on TQ_REMOTE_LOG_ENABLE - help - Keep the original ESP-IDF vprintf output while also - enqueueing logs for server upload. - - config TQ_REMOTE_LOG_QUEUE_LEN - int "Remote log memory queue length" - default 128 - range 16 512 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_ENTRY_MAX_BYTES - int "Remote log entry max bytes" - default 256 - range 128 1024 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_BATCH_MAX_ENTRIES - int "Remote log batch max entries" - default 30 - range 5 100 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_FLUSH_INTERVAL_MS - int "Remote log flush interval" - default 5000 - range 1000 60000 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS - int "Remote log upload timeout" - default 8000 - range 3000 30000 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_MAX_FILES - int "Remote log persisted file count" - default 32 - range 4 128 - depends on TQ_REMOTE_LOG_ENABLE - - config TQ_REMOTE_LOG_FILE_MAX_BYTES - int "Remote log persisted file max bytes" - default 16384 - range 4096 65536 - depends on TQ_REMOTE_LOG_ENABLE - - endmenu - menu "Buttons & Input" config TQ_KEY_INPUT_ENABLE diff --git a/remote-logging-system-design.md b/remote-logging-system-design.md deleted file mode 100644 index c777a7e..0000000 --- a/remote-logging-system-design.md +++ /dev/null @@ -1,539 +0,0 @@ -# TQ_catN 远程日志系统设计方案 - -**版本**:v1.0(待审核) -**日期**:2026-06-27 -**范围**:全设备日志收集、本地缓冲、网络上传 - ---- - -## 一、设计目标 - -| 目标 | 说明 | -|------|------| -| **可靠性** | 网络抖动或断开时,日志不丢失,具备本地持久化和重传能力 | -| **低开销** | 对实时语音交互(PTT/WS/audio)零干扰,CPU 占用 < 5%,内存占用 < 256KB | -| **分层合规** | 严格遵循现有四层单向依赖架构(platform → domain → control_plane → app_composition) | -| **可配置** | 支持按模块/级别动态开关,支持生产环境静默/调试环境全量 | -| **安全性** | 日志上传使用 HTTPS + device JWT 认证,敏感字段自动脱敏 | - ---- - -## 二、架构设计 - -### 2.1 与现有分层架构的集成 - -``` -app_composition - └── 初始化 remote_log_runtime(在 boot sequence 中启动) - -control_plane - └── remote_log_policy.c # 策略:上传触发条件、级别控制、模块过滤 - └── remote_log_config.h # 模块/级别开关配置 - -domain - └── remote_log_format.c # 日志格式化:JSON 序列化、时间戳、device_id 注入 - └── remote_log_compress.c # 可选:压缩(miniz/lz4) - -platform - └── remote_log_buffer.c # 双缓冲:内存 ring buffer + 文件队列(LittleFS) - └── remote_log_transport.c # 网络传输:复用 platform_https_post_json - └── remote_log_hook.c # ESP-IDF log hook:esp_log_set_vprintf 重定向 -``` - -**依赖方向**: -- platform 层提供底层能力(buffer、transport、hook) -- domain 层提供格式化/压缩(无状态转换) -- control_plane 层提供策略(何时上传、上传什么) -- app_composition 层负责初始化编排 - -**禁止**:control_plane / domain 直接调用 esp_log_set_vprintf 或 esp_http_client API,必须通过 platform facade。 - ---- - -### 2.2 数据流 - -``` -[App Code: ESP_LOGI/ESP_LOGE] - │ - ▼ -[platform/remote_log_hook.c] ──→ 拦截 ESP-IDF 日志输出 - │ (原始串口输出保留,不影响调试) - ▼ -[platform/remote_log_buffer.c] - ├── 内存环形缓冲区(热路径,ISR/任务安全) - │ └── 容量:64KB(PSRAM 优先,内部 RAM 回退) - │ - └── 文件队列(冷路径,网络断开时持久化) - └── 容量:512KB(LittleFS gifpack 分区空闲空间) - └── 最大文件数:32 个,每个 16KB - └── 命名:log_batch_001.jsonl ~ log_batch_032.jsonl - │ - ▼(满足上传条件时) -[domain/remote_log_format.c] - └── 封装为 JSON batch: - { - "device_id": "66c...", - "batch_id": "uuid", - "timestamp_ms": 1782552243000, - "logs": [ - {"ts": 1782552243001, "level": "E", "tag": "cp_mm_session", "msg": "..."}, - ... - ] - } - │ - ▼ -[domain/remote_log_compress.c] ──→ 可选:miniz 压缩(压缩率 ~60%) - │ - ▼ -[platform/remote_log_transport.c] - └── 复用 platform_https_post_json() → 服务端 - POST https://gz.api.talkingq.com/log/batch - Content-Type: application/json - Authorization: Bearer -``` - ---- - -## 三、核心模块设计 - -### 3.1 日志拦截层(platform/remote_log_hook.c) - -**方案**:使用 ESP-IDF 的 `esp_log_set_vprintf()` 钩子,在保留原有串口输出的同时,将日志复制到远程日志缓冲区。 - -```c -// 接口设计 -void platform_remote_log_hook_install(void); -void platform_remote_log_hook_uninstall(void); - -// 内部实现 -static int remote_log_vprintf(const char *fmt, va_list args) -{ - // 1. 调用原始 vprintf,保持串口输出 - int ret = s_original_vprintf(fmt, args); - - // 2. 格式化为结构化记录,写入 ring buffer - // 使用非阻塞写入,buffer 满时丢弃最旧记录 - platform_remote_log_buffer_write(fmt, args); - - return ret; -} -``` - -**关键约束**: -- `remote_log_vprintf` 可能在 ISR 上下文被调用(如 `ESP_EARLY_LOG`),因此 `platform_remote_log_buffer_write` 内部必须: - - 使用 `xRingbufferSendFromISR` 或自旋锁保护的环形缓冲区 - - 禁止任何可能阻塞的操作(malloc、semaphore take 等) -- 如果检测到 ISR 上下文,只做**原子计数 + 标记**,由后台任务在退出 ISR 后批量拷贝 - -**日志级别过滤**:在 hook 层不做级别过滤(避免重复判断),过滤由 control_plane 策略层通过 Kconfig 宏控制编译期开关。 - ---- - -### 3.2 双缓冲层(platform/remote_log_buffer.c) - -#### 3.2.1 内存环形缓冲区(热路径) - -```c -#define REMOTE_LOG_RING_BUFFER_SIZE (64 * 1024) // 64KB -#define REMOTE_LOG_RING_BLOCK_SIZE 256 // 每条日志最大 256 字节(超长截断) - -static uint8_t s_ring_buffer[REMOTE_LOG_RING_BUFFER_SIZE]; // PSRAM 优先分配 -static volatile size_t s_ring_write_idx; -static volatile size_t s_ring_read_idx; -``` - -**设计**: -- 使用简单的无锁环形缓冲区(单生产者:hook ISR/任务;单消费者:后台 flush 任务) -- 写入:原子递增 write_idx,memcpy 数据 -- 读取:后台任务定期(100ms)将数据从 ring buffer 搬到文件队列 -- 满策略:丢弃最旧记录(覆盖 read_idx 指向的数据),避免阻塞生产者 - -#### 3.2.2 文件队列(冷路径 / 持久化) - -```c -#define REMOTE_LOG_FILE_MAX_BATCHES 32 -#define REMOTE_LOG_FILE_BATCH_SIZE (16 * 1024) // 16KB / 文件 -``` - -**设计**: -- 使用 LittleFS 的 `gifpack` 分区空闲空间(当前分区 8MB,OTA 后仍有富余) -- 文件名:`/littlefs/log/batch_001.jsonl` ~ `/littlefs/log/batch_032.jsonl` -- 写入格式:JSON Lines(每行一条 JSON,便于追加和断点续传) -- 循环覆盖:当 32 个文件都写满后,覆盖最早的文件 - -**掉电保护**: -- 每个 batch 文件写完后调用 `fsync`,确保数据落盘 -- 启动时扫描 `/littlefs/log/` 目录,恢复未上传的 batch - ---- - -### 3.3 格式化层(domain/remote_log_format.c) - -**输入**:原始 ESP-IDF 日志字符串(如 `I (1934379) cp_mm_session: Transport unavailable`) - -**输出**:结构化 JSON batch - -```c -typedef struct { - int64_t timestamp_ms; // SNTP 同步后的毫秒时间戳 - char level; // 'V'/'D'/'I'/'W'/'E' - char tag[32]; // 模块 TAG - char message[192]; // 日志内容(超长截断) -} remote_log_entry_t; -``` - -**批处理策略**: -- 每批最多 50 条日志 或 每批最多 8KB 数据(先到为准) -- 超时机制:即使不满 50 条,最多等待 5 秒后强制上传 -- 压缩:使用 miniz(zlib 兼容,纯 C,~8KB 代码)压缩 JSON batch,减少流量 50-70% - -**脱敏规则**: -- 自动匹配并替换:`password=xxx`、`Bearer xxx`、`token=xxx` -- 使用正则或简单字符串扫描:`s/([Pp]assword|[Tt]oken|[Bb]earer)[=:]\S+/\1=***REDACTED***/g` - ---- - -### 3.4 传输层(platform/remote_log_transport.c) - -**复用现有能力**: -- HTTP 客户端:复用 `platform_https_post_json()`(已有 TLS、DNS、超时处理) -- 认证:复用 device JWT token(已有 `control_plane_multimodal_auth_service`) - -**上传策略**: -```c -typedef struct { - const char *endpoint_url; // 如 "https://gz.api.talkingq.com/log/batch" - uint32_t batch_max_count; // 50 - uint32_t batch_max_size_bytes; // 8192 - uint32_t flush_interval_ms; // 5000 - uint32_t upload_timeout_ms; // 10000 - uint32_t max_retry_count; // 3 - uint32_t retry_backoff_ms; // 2000 -} remote_log_transport_config_t; -``` - -**上传流程**: -1. 检查 Wi-Fi 是否连接(`control_plane_is_sta_connected()`) -2. 检查 device token 是否有效(复用 auth service) -3. 读取一批日志(优先内存 buffer,其次文件队列) -4. 封装 JSON batch + device_id + batch_id -5. 调用 `platform_https_post_json()` 上传 -6. 上传成功:删除已上传的文件 batch / 释放内存 buffer -7. 上传失败(非 401):按指数退避重试(2s / 4s / 8s),失败后回写文件队列 -8. 上传失败(401):触发 device token 刷新,刷新成功后重试 - -**网络降级**: -- WS 连接活跃时(用户正在对话):暂停日志上传,避免干扰实时音频/WS 流量 -- Wi-Fi 断开时:完全停止上传,仅写入文件队列 -- 内存 buffer 满且文件队列满时:丢弃最旧记录(采样策略:优先保留 Error/Warning) - ---- - -### 3.5 策略层(control_plane/remote_log_policy.c) - -**职责**:控制"上传什么、何时上传、上传多少" - -```c -typedef enum { - REMOTE_LOG_LEVEL_NONE = 0, - REMOTE_LOG_LEVEL_ERROR = 1, // 仅 Error - REMOTE_LOG_LEVEL_WARN = 2, // Error + Warning - REMOTE_LOG_LEVEL_INFO = 3, // Error + Warning + Info - REMOTE_LOG_LEVEL_DEBUG = 4, // 全部 -} remote_log_level_t; - -// 模块级别开关(与现有 TAG 对齐) -typedef struct { - bool cp_mm_session; // control_plane multimodal session - bool cp_mm_runtime; // control_plane multimodal runtime - bool cp_workers; // control_plane runtime workers - bool cp_connectivity; // control_plane connectivity - bool transport_ws; // platform transport ws - bool audio_pipeline; // platform audio pipeline - bool direction; // domain direction - bool fortune; // domain fortune - // ... 其他模块 -} remote_log_module_mask_t; -``` - -**策略规则**: -- 生产环境默认:`LEVEL_ERROR`,仅关键模块开启 -- 调试环境默认:`LEVEL_DEBUG`,全部模块开启 -- 动态调整:通过 LAN HTTP 配置页面(复用 `domain_user_config_server`)下发策略变更 - ---- - -## 四、Kconfig 配置项 - -建议在 `main/Kconfig.projbuild` 中新增菜单: - -```kconfig -menu "Remote Logging" - - config TQ_REMOTE_LOG_ENABLED - bool "Enable remote logging" - default n - help - Enable log collection and remote upload. Increases flash - usage by ~24KB and RAM usage by ~80KB. - - config TQ_REMOTE_LOG_LEVEL - int "Default log level for remote upload" - default 1 - range 0 4 - help - 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG - - config TQ_REMOTE_LOG_RING_BUFFER_SIZE - int "Memory ring buffer size (KB)" - default 64 - range 8 256 - - config TQ_REMOTE_LOG_FILE_BATCHES - int "Max file batches on LittleFS" - default 32 - range 8 64 - - config TQ_REMOTE_LOG_BATCH_MAX_COUNT - int "Max log entries per batch" - default 50 - range 10 200 - - config TQ_REMOTE_LOG_FLUSH_INTERVAL_MS - int "Flush interval (ms)" - default 5000 - range 1000 30000 - - config TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS - int "Upload HTTP timeout (ms)" - default 10000 - range 3000 30000 - - config TQ_REMOTE_LOG_COMPRESS - bool "Enable miniz compression" - default y - help - Compress log batches before upload. Reduces data usage - by ~60% at the cost of ~8KB flash and ~2KB RAM. - - config TQ_REMOTE_LOG_ENDPOINT - string "Log upload endpoint URL" - default "https://gz.api.talkingq.com/log/batch" - help - HTTPS endpoint for log batch upload. - -endmenu -``` - ---- - -## 五、内存和性能预算 - -### 5.1 Flash 占用 - -| 组件 | 代码+数据 | 说明 | -|------|----------|------| -| remote_log_hook.c | ~4KB | 钩子 + ring buffer 管理 | -| remote_log_buffer.c | ~6KB | 双缓冲 + LittleFS 文件操作 | -| remote_log_transport.c | ~4KB | HTTP 上传 + 重试逻辑 | -| remote_log_format.c | ~6KB | JSON 序列化 + 脱敏 | -| remote_log_compress.c | ~8KB | miniz 压缩库 | -| remote_log_policy.c | ~2KB | 策略判断 + 模块过滤 | -| **合计** | **~30KB** | 可配置关闭压缩节省 8KB | - -### 5.2 RAM 占用(运行时) - -| 组件 | 内存 | 位置 | -|------|------|------| -| Ring buffer | 64KB | PSRAM(配置为 PSRAM 优先) | -| Upload working buffer | 8KB | PSRAM | -| HTTP client(复用现有) | ~4KB | 内部 RAM(复用已有实例) | -| 格式化/压缩临时区 | ~4KB | PSRAM | -| **合计** | **~80KB** | 其中内部 RAM < 10KB | - -### 5.3 CPU 占用 - -| 场景 | CPU 占用 | -|------|----------| -| 日志写入(hook) | < 1%(纯内存拷贝) | -| 定时 flush(100ms) | < 2%(文件写入) | -| 上传(每 5s 一次) | < 5%(JSON 格式化 + HTTP POST) | -| 用户对话期间(WS 活跃) | 0%(暂停上传) | - ---- - -## 六、启动时序 - -在 `app_composition_default_runtime.c` 的 boot sequence 中,在 `control_plane_init()` 之后、`wifi_bootstrap` 之前插入: - -```c -// 步骤 7.5:初始化远程日志系统 -esp_err_t log_err = control_plane_init_remote_logging(); -if (log_err != ESP_OK) { - ESP_LOGW(TAG, "Remote logging init failed: %s", esp_err_to_name(log_err)); - // 非致命错误,继续启动 -} -``` - -**理由**: -- 在 `control_plane_init()` 之后:确保 device auth 已加载(用于上传认证) -- 在 `wifi_bootstrap` 之前:确保 Wi-Fi 连接建立后日志系统已就绪,可以立即开始收集 - ---- - -## 七、服务端接口契约 - -### 7.1 上传接口 - -```http -POST /log/batch -Content-Type: application/json -Authorization: Bearer - -{ - "device_id": "66c7f8a2e4b0d1e2f3a4b5c6", - "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "timestamp_ms": 1782552243000, - "firmware_version": "1.2.3", - "logs": [ - { - "ts": 1782552243001, - "level": "E", - "tag": "transport_ws", - "msg": "Sec-WebSocket-Accept not found" - }, - { - "ts": 1782552243010, - "level": "W", - "tag": "cp_mm_session", - "msg": "Transport unavailable: reason=ws_error" - } - ] -} -``` - -### 7.2 服务端响应 - -```http -HTTP/1.1 200 OK -Content-Type: application/json - -{ - "code": 200, - "message": "OK", - "data": { - "accepted": 2, - "dropped": 0 - } -} -``` - -### 7.3 错误码 - -| HTTP 状态码 | 含义 | 客户端行为 | -|------------|------|-----------| -| 200 | 成功 | 删除已上传的 batch | -| 400 | 格式错误 | 丢弃当前 batch(避免无限重试) | -| 401 | 认证失败 | 刷新 device JWT 后重试 | -| 413 | Payload 过大 | 减小 batch size 后重试 | -| 429 | 频率限制 | 指数退避后重试 | -| 500+ | 服务端错误 | 指数退避后重试 | - ---- - -## 八、安全与隐私 - -### 8.1 认证 -- 复用现有 device JWT 认证体系,不引入新密钥 -- token 过期时自动刷新(复用 `control_plane_multimodal_auth_service`) - -### 8.2 传输 -- 强制 HTTPS,复用现有 TLS 证书校验(`esp-x509-crt-bundle`) -- 禁止 HTTP 明文传输 - -### 8.3 脱敏 -- 自动脱敏:password、token、Bearer、secret、api_key 等前缀后的值 -- 用户配置:支持通过 LAN HTTP 配置页面自定义脱敏关键词 - -### 8.4 数据保留 -- 本地:最多保留 32 个 batch 文件(~512KB),循环覆盖 -- 服务端:建议保留 30 天,过期自动清理 - ---- - -## 九、故障排查与调试 - -### 9.1 本地诊断命令 -通过现有的 factory CLI 或 LAN HTTP 配置页面,增加以下诊断接口: - -``` -GET /api/diag/log/status -→ { - "enabled": true, - "level": 2, - "ring_buffer_used": 32768, - "ring_buffer_total": 65536, - "file_batches_pending": 5, - "last_upload_status": "success", - "last_upload_time": "2026-06-27T10:30:00Z" - } -``` - -### 9.2 本地日志导出 -``` -GET /api/diag/log/download?batch=latest -→ 返回最近一个 batch 文件的内容(纯文本) -``` - ---- - -## 十、实施建议 - -### Phase 1(最小可用) -1. `platform/remote_log_hook.c` — 拦截 ESP_LOG 到 ring buffer -2. `platform/remote_log_buffer.c` — 内存缓冲 + 文件持久化 -3. `platform/remote_log_transport.c` — 复用 HTTPS 上传 -4. `control_plane/remote_log_policy.c` — 级别过滤 + 模块开关 -5. Kconfig 配置项 + boot sequence 初始化 - -### Phase 2(增强) -6. `domain/remote_log_format.c` — JSON 格式化 + 脱敏 -7. `domain/remote_log_compress.c` — miniz 压缩 -8. LAN HTTP 配置页面集成(动态调整级别) -9. 诊断接口(`/api/diag/log/status`) - -### Phase 3(优化) -10. 服务端实时告警(Error 级别日志触发服务端告警) -11. 日志采样(高频 Debug 日志按概率采样) -12. 日志关联(与 device session_id 关联,便于追踪单设备全链路) - ---- - -## 十一、风险评估 - -| 风险 | 缓解措施 | -|------|----------| -| 日志写入拖慢 ISR | hook 层使用纯内存拷贝 + 无锁 ring buffer,禁止 malloc | -| 文件写入磨损 Flash | LittleFS 磨损均衡,日志 batch 文件大小控制在 16KB | -| 上传流量过大 | 压缩(-60%)+ 级别过滤(仅 ERROR/WARN)+ 批量上传(5s 间隔) | -| 上传干扰 WS 音频 | WS 活跃时暂停上传,仅写入本地 buffer | -| 敏感信息泄露 | 自动脱敏 + HTTPS + 服务端 ACL | -| 内存不足 | PSRAM 优先分配,内部 RAM 占用 < 10KB | - ---- - -## 十二、总结 - -本方案基于现有四层架构,以 **"零侵入、低开销、高可靠"** 为核心原则: - -- **platform 层**:提供日志拦截、双缓冲、网络传输的底层能力 -- **domain 层**:提供 JSON 格式化、压缩、脱敏的业务处理 -- **control_plane 层**:提供级别控制、模块过滤、上传策略的策略决策 -- **app_composition 层**:负责启动编排 - -关键设计决策: -1. 复用 `platform_https_post_json()`,不引入新 HTTP 客户端 -2. 复用 device JWT,不引入新认证体系 -3. 使用 `esp_log_set_vprintf()` 拦截,不修改现有 ESP_LOGx 调用 -4. 使用 LittleFS 持久化,不引入新文件系统 -5. 用户对话期间暂停上传,保证实时语音不受干扰 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index db5d7de..a2e734d 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -63,25 +63,11 @@ CONFIG_TQ_LIGHT_STANDBY_PROMPT_FILE="standby_5min.opus" CONFIG_TQ_LIGHT_STANDBY_DISABLE_SPEAKER_OUTPUT=y CONFIG_TQ_LIGHT_STANDBY_RELEASE_MOTOR_HOLD=y CONFIG_TQ_LIGHT_STANDBY_WIFI_POWER_SAVE=y -CONFIG_TQ_LIGHT_STANDBY_PAUSE_REMOTE_LOG_UPLOAD=y -CONFIG_TQ_LIGHT_STANDBY_REMOTE_LOG_POLL_MS=5000 CONFIG_TQ_LIGHT_STANDBY_BATTERY_INTERVAL_SEC=300 CONFIG_TQ_LIGHT_STANDBY_MEMORY_SAMPLE_INTERVAL_MS=10000 CONFIG_TQ_LIGHT_STANDBY_MEMORY_REPORT_INTERVAL_MS=300000 CONFIG_TQ_LIGHT_STANDBY_TASK_STACK_BYTES=4096 CONFIG_TQ_LIGHT_STANDBY_TASK_PRIORITY=3 -CONFIG_TQ_REMOTE_LOG_ENABLE=y -CONFIG_TQ_REMOTE_LOG_ENDPOINT="http://47.98.175.183:8090/log/batch" -CONFIG_TQ_REMOTE_LOG_AUTH_TOKEN="" -CONFIG_TQ_REMOTE_LOG_MIN_LEVEL=2 -CONFIG_TQ_REMOTE_LOG_PRESERVE_LOCAL_OUTPUT=y -CONFIG_TQ_REMOTE_LOG_QUEUE_LEN=128 -CONFIG_TQ_REMOTE_LOG_ENTRY_MAX_BYTES=256 -CONFIG_TQ_REMOTE_LOG_BATCH_MAX_ENTRIES=30 -CONFIG_TQ_REMOTE_LOG_FLUSH_INTERVAL_MS=5000 -CONFIG_TQ_REMOTE_LOG_UPLOAD_TIMEOUT_MS=8000 -CONFIG_TQ_REMOTE_LOG_MAX_FILES=12 -CONFIG_TQ_REMOTE_LOG_FILE_MAX_BYTES=16384 CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y # CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set # CONFIG_ESP_COREDUMP_ENABLE_TO_NONE is not set diff --git a/tools/remote_log_receiver.py b/tools/remote_log_receiver.py deleted file mode 100644 index 4e2c566..0000000 --- a/tools/remote_log_receiver.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 -"""Minimal HTTP receiver for TQ remote log batches. - -Usage: - python tools/remote_log_receiver.py --host 0.0.0.0 --port 8080 --dir ./remote_logs - -Configure the device endpoint as: - http://:8080/log/batch - -For production, put this behind HTTPS or a reverse proxy that terminates TLS. -""" - -from __future__ import annotations - -import argparse -import datetime as dt -import json -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path - - -class LogReceiver(BaseHTTPRequestHandler): - server_version = "TQRemoteLogReceiver/1.0" - - def do_POST(self) -> None: - if self.path != "/log/batch": - self._send_json(404, {"code": 404, "message": "not found"}) - return - - length = int(self.headers.get("Content-Length", "0")) - if length <= 0 or length > self.server.max_body_bytes: - self._send_json(413, {"code": 413, "message": "payload too large"}) - return - - raw = self.rfile.read(length) - try: - payload = json.loads(raw.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError) as exc: - self._send_json(400, {"code": 400, "message": f"invalid json: {exc}"}) - return - - now = dt.datetime.now(dt.timezone.utc) - day_dir = self.server.output_dir / now.strftime("%Y%m%d") - day_dir.mkdir(parents=True, exist_ok=True) - - device_id = str(payload.get("device_id") or "unknown").replace("/", "_") - batch_id = str(payload.get("batch_id") or now.strftime("%H%M%S%f")).replace("/", "_") - json_path = day_dir / f"{device_id}_{batch_id}.json" - jsonl_path = day_dir / f"{device_id}.jsonl" - - with json_path.open("w", encoding="utf-8") as file: - json.dump(payload, file, ensure_ascii=False, indent=2) - file.write("\n") - - entries = payload.get("entries") or [] - with jsonl_path.open("a", encoding="utf-8") as file: - for entry in entries: - row = { - "received_at": now.isoformat(), - "device_id": payload.get("device_id"), - "serial_number": payload.get("serial_number"), - "batch_id": payload.get("batch_id"), - **entry, - } - file.write(json.dumps(row, ensure_ascii=False, separators=(",", ":"))) - file.write("\n") - - self._send_json( - 200, - { - "code": 200, - "message": "OK", - "data": { - "accepted": len(entries), - "stored": str(json_path), - }, - }, - ) - - def log_message(self, fmt: str, *args: object) -> None: - print(f"{self.address_string()} - {fmt % args}") - - def _send_json(self, status: int, payload: dict) -> None: - body = json.dumps(payload, ensure_ascii=False).encode("utf-8") - self.send_response(status) - self.send_header("Content-Type", "application/json; charset=utf-8") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - -class LogServer(ThreadingHTTPServer): - def __init__(self, server_address: tuple[str, int], handler_class, output_dir: Path, max_body_bytes: int): - super().__init__(server_address, handler_class) - self.output_dir = output_dir - self.max_body_bytes = max_body_bytes - - -def main() -> None: - parser = argparse.ArgumentParser(description="Receive TQ device remote log batches.") - parser.add_argument("--host", default="0.0.0.0") - parser.add_argument("--port", type=int, default=8080) - parser.add_argument("--dir", type=Path, default=Path("remote_logs")) - parser.add_argument("--max-body-bytes", type=int, default=256 * 1024) - args = parser.parse_args() - - args.dir.mkdir(parents=True, exist_ok=True) - server = LogServer((args.host, args.port), LogReceiver, args.dir, args.max_body_bytes) - print(f"listening on http://{args.host}:{args.port}/log/batch") - print(f"writing logs under {args.dir.resolve()}") - server.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/tools/view_flash_logs.command b/tools/view_flash_logs.command deleted file mode 100755 index 5a70355..0000000 --- a/tools/view_flash_logs.command +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -LOGFS_OFFSET="0x210000" -LOGFS_SIZE="0x40000" -LOGFS_BLOCK_SIZE="4096" - -pause_before_exit() { - if [[ -t 0 ]]; then - printf '\n按回车键关闭窗口...' - read -r _ - fi -} - -fail() { - printf '\n错误:%s\n' "$1" >&2 - pause_before_exit - exit 1 -} - -find_serial_port() { - local requested_port="${1:-}" - if [[ -n "$requested_port" ]]; then - [[ -e "$requested_port" ]] || fail "串口不存在:$requested_port" - printf '%s' "$requested_port" - return - fi - - local ports=() - local candidate - for candidate in /dev/cu.usbmodem* /dev/cu.usbserial* \ - /dev/cu.SLAB_USBtoUART* /dev/cu.wchusbserial*; do - [[ -e "$candidate" ]] && ports+=("$candidate") - done - - if [[ ${#ports[@]} -eq 0 ]]; then - fail "没有检测到设备串口。请连接设备后重试。" - fi - if [[ ${#ports[@]} -eq 1 ]]; then - printf '%s' "${ports[0]}" - return - fi - - printf '检测到多个串口:\n' >&2 - local index - for ((index = 0; index < ${#ports[@]}; index++)); do - printf ' %d) %s\n' "$((index + 1))" "${ports[index]}" >&2 - done - printf '请选择序号:' >&2 - local selection - read -r selection - [[ "$selection" =~ ^[0-9]+$ ]] || fail "输入的序号无效。" - ((selection >= 1 && selection <= ${#ports[@]})) || fail "输入的序号超出范围。" - printf '%s' "${ports[selection - 1]}" -} - -find_build_tool() { - local relative_path="$1" - local candidate="$PROJECT_DIR/$relative_path" - if [[ -x "$candidate" ]]; then - printf '%s' "$candidate" - return - fi - return 1 -} - -find_idf_value() { - local key="$1" - local cache="$PROJECT_DIR/build/CMakeCache.txt" - [[ -f "$cache" ]] || return 1 - sed -n "s|^${key}:[^=]*=||p" "$cache" | head -n 1 -} - -find_idf_python() { - local candidate - candidate="$(find_idf_value PYTHON || true)" - if [[ -n "$candidate" && -x "$candidate" ]]; then - printf '%s' "$candidate" - return - fi - - for candidate in "$HOME"/.espressif/tools/python/*/venv/bin/python; do - if [[ -x "$candidate" ]]; then - printf '%s' "$candidate" - return - fi - done - return 1 -} - -find_esptool() { - local idf_path - idf_path="$(find_idf_value esp-idf_SOURCE_DIR || true)" - local candidate="$idf_path/components/esptool_py/esptool/esptool.py" - if [[ -n "$idf_path" && -f "$candidate" ]]; then - printf '%s' "$candidate" - return - fi - - for candidate in "$HOME"/.espressif/v*/esp-idf/components/esptool_py/esptool/esptool.py; do - if [[ -f "$candidate" ]]; then - printf '%s' "$candidate" - return - fi - done - return 1 -} - -PORT="$(find_serial_port "${1:-}")" -IDF_PYTHON="$(find_idf_python || true)" -ESPTOOL="$(find_esptool || true)" -LITTLEFS_TOOL="$(find_build_tool build/littlefs_py_venv/bin/littlefs-python || true)" - -[[ -n "$IDF_PYTHON" ]] || fail "找不到 ESP-IDF Python 环境,请先成功构建一次工程。" -[[ -n "$ESPTOOL" ]] || fail "找不到 esptool.py,请检查 ESP-IDF 是否已安装。" -[[ -n "$LITTLEFS_TOOL" ]] || fail "找不到 LittleFS 解包工具,请先成功构建一次工程。" - -TIMESTAMP="$(date '+%Y%m%d-%H%M%S')" -OUTPUT_DIR="$PROJECT_DIR/flash_log_dump/$TIMESTAMP" -IMAGE_PATH="$OUTPUT_DIR/logfs.bin" -EXTRACT_DIR="$OUTPUT_DIR/extracted" -REPORT_PATH="$OUTPUT_DIR/remote_logs.txt" -mkdir -p "$EXTRACT_DIR" - -printf '设备串口:%s\n' "$PORT" -printf '输出目录:%s\n' "$OUTPUT_DIR" -printf '\n注意:请先关闭正在使用该串口的 monitor。\n' -printf '读取 Flash 会让设备进入下载模式,完成后设备会自动重启。\n' -printf '正在读取 logfs 分区,请勿拔线...\n\n' - -if ! "$IDF_PYTHON" "$ESPTOOL" \ - --chip esp32s3 \ - --port "$PORT" \ - --baud 460800 \ - --before default_reset \ - --after hard_reset \ - read_flash "$LOGFS_OFFSET" "$LOGFS_SIZE" "$IMAGE_PATH"; then - fail "读取 Flash 失败。请关闭 monitor、检查 USB 连接后重试。" -fi - -printf '\n正在解包 LittleFS...\n' -if ! "$LITTLEFS_TOOL" extract \ - --block-size "$LOGFS_BLOCK_SIZE" \ - "$IMAGE_PATH" "$EXTRACT_DIR"; then - fail "LittleFS 解包失败,请确认设备使用的是当前 16m.csv 分区表。" -fi - -if ! "$IDF_PYTHON" - "$EXTRACT_DIR" "$REPORT_PATH" <<'PY' -import json -import sys -from pathlib import Path - -extract_dir = Path(sys.argv[1]) -report_path = Path(sys.argv[2]) -log_dir = extract_dir / "remote_logs" -log_files = sorted(log_dir.glob("log_*.jsonl")) if log_dir.is_dir() else [] - -with report_path.open("w", encoding="utf-8") as report: - report.write("TalkingQ cached remote logs\n") - report.write(f"Source: {log_dir}\n") - report.write(f"Files: {len(log_files)}\n\n") - - if not log_files: - report.write("No pending remote log files were found.\n") - - for log_file in log_files: - report.write(f"===== {log_file.name} =====\n") - with log_file.open("r", encoding="utf-8", errors="replace") as source: - for raw_line in source: - line = raw_line.strip() - if not line: - continue - try: - entry = json.loads(line) - timestamp = entry.get("ts_ms", "?") - level = entry.get("level", "?") - truncated = " [truncated]" if entry.get("truncated") else "" - message = entry.get("message", "") - report.write(f"[{timestamp:>10} ms] {level}{truncated} {message}\n") - except (json.JSONDecodeError, TypeError, ValueError): - report.write(f"[invalid JSON] {line}\n") - report.write("\n") -PY -then - fail "生成可读日志文件失败。" -fi - -printf '\n读取完成:%s\n' "$REPORT_PATH" -if command -v open >/dev/null 2>&1; then - open "$REPORT_PATH" -else - printf '当前系统无法自动打开文件,请手动查看上述路径。\n' -fi - -pause_before_exit