refactor(logging): remove remote logging subsystem
This commit is contained in:
@@ -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,
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 数据”帧格式,逐帧送入统一的下行播放管线。 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 刷新和音频下行链路是否值得进入下一轮优化。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. 用户对话期间暂停上传,保证实时语音不受干扰
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user