修复了WS连接稳定性问题

This commit is contained in:
CSBOOK\44744
2026-06-27 20:29:09 +08:00
parent b829cd8ebc
commit caa4a7eded
16 changed files with 2197 additions and 248 deletions
+539
View File
@@ -0,0 +1,539 @@
# 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. 用户对话期间暂停上传,保证实时语音不受干扰