refactor(logging): remove remote logging subsystem

This commit is contained in:
chenjintao
2026-10-08 16:08:41 +08:00
parent 5d849033c3
commit 3160a33f50
22 changed files with 12 additions and 2546 deletions
-1
View File
@@ -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,
1 # ESP-IDF Partition Table
4 otadata, data, ota, 0xd000, 0x2000,
5 phy_init, data, phy, 0xf000, 0x1000,
6 gifpack, data, littlefs, , 0x200000,
logfs, data, littlefs, , 0x40000,
7 ota_0, app, ota_0, , 0x380000,
8 ota_1, app, ota_1, , 0x380000,
9 model, data, spiffs, , 0x300000,
-1
View File
@@ -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"
@@ -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);
@@ -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 {
@@ -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
File diff suppressed because it is too large Load Diff
@@ -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);
}
-1
View File
@@ -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,
@@ -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,
-1
View File
@@ -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,
@@ -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 数据”帧格式,逐帧送入统一的下行播放管线。 */
+2 -41
View File
@@ -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
```
@@ -1,5 +1,7 @@
# Flash 与内存容量分析、风险及后续开发建议
> 历史容量快照:文中远程日志与 `logfs` 已从当前固件移除,分区地址和容量应以现行 `16m.csv` 与最新构建产物为准。
本文记录当前固件的 Flash 分区使用情况、静态 RAM 使用情况、运行时内存风险、可优化内容和后续开发约束,用于功能评审、资源接入、OTA 设计和长期容量管理。
LittleFS 资源从源码到设备的详细流程见:
@@ -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 能继续上传,而不是永久被阻塞。
@@ -14,7 +14,7 @@
- Wi-Fi 连接、重连和配置热点;
- 设备身份、JWT 和会话令牌;
- 用户出生资料、每日运势和财富方向;
- NVS、LittleFS、远程日志和工厂 CLI。
- NVS、LittleFS 和工厂 CLI。
## 2. 分层架构
@@ -269,7 +269,6 @@ flowchart TB
LCD["cp_lcd_render<br/>LCD 启动"]
Events["cp_system_event<br/>事件分发"]
Keys["cp_key_input<br/>GPIO 与消抖"]
Logs["remote log worker"]
Fortune["cp_fortune<br/>每日运势"]
Monitor["cp_task_monitor<br/>可选诊断"]
@@ -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`
@@ -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
+3 -55
View File
@@ -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 刷新和音频下行链路是否值得进入下一轮优化。
-96
View File
@@ -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
<token>. 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
-539
View File
@@ -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 <device_token>
```
---
## 三、核心模块设计
### 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_jwt_token>
{
"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. 用户对话期间暂停上传,保证实时语音不受干扰
-14
View File
@@ -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
-115
View File
@@ -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://<server-ip>: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()
-198
View File
@@ -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