Rdzleo fc07d3806d Phase 01 调试迭代: OV3660 人脸检测集成 + 23 条踩坑经验汇总
## 代码变更

### main/application.cc
修复 T01 probe 日志 %lld 格式 bug(改用 %lu + unsigned long)

### main/boards/common/esp32_camera.cc
- 修复 DVP V4L2 单 buffer 导致 DMA 饥饿:req.count 从 1 改为 2
- 修复 [T01] Probe 日志 elapsed=ldus 显示问题(同上格式修复)

### main/face_tracker.cc
多轮迭代:
- 新增 frame debug 诊断日志(打印 top-left/center 16B + zero_bytes 统计)
- pix_type 尝试路径:YUYV → RGB565LE → RGB565BE → YUYV → RGB888(手动转换)
- 手动实现 BT.601 公式 YUYV→RGB888 转换,绕过 ImagePreprocessor 黑盒
- face_tracker 任务从 Core 0 切换到 Core 1,避让 RMT/LED 死锁
- 新增 INFO 级限频日志(每秒 1 条 face 检测记录)
- 修复推理时长日志 %lld 格式 bug
- 连续 3 秒无人脸时打印 no face detected

### main/idf_component.yml
esp_video 升级 1.3.1 → ~1.4.1(手动 patch 修 xclk_freq bug)

### partitions/v2/16m.csv
OTA 分区扩容:3.94MB → 5MB,assets 缩到 5.875MB,支持 4.23MB 固件

### docs/phase-01-face-tracking/PROGRESS.md
更新 Phase 01 执行日志,记录实机调试细节

## 文档更新

### Coglet项目分析与开发指南.md 新增第六点五节

完整记录本轮调试的 23 个踩坑,分为:
1. 编译/配置类(5 个):板级重置、依赖冲突、bootloader 缓存、%lld 格式、xclk_freq bug
2. 摄像头数据链路(5 个):sensor driver 启用、V4L2 buffer 数量、分区扩容、镜头保护膜、光照
3. esp-dl 人脸检测(3 个):MSR letterbox 伪影、ESPDET OOD 默认输出、字节序判断
4. 任务调度(3 个):WDT 崩溃、GDMA ISR 崩溃、弱符号链接
5. RP2040 端(4 个):idle 回中、坐标累加撞限位、mpremote 阻塞、两分支代码差异
6. 硬件(3 个):飞线验证、360° 舵机误用、烧录生效验证

附调试方法论 6 条 + 未解决遗留问题 3 条

## 已解决问题

-  ESP-IDF 编译链路(依赖/分区/格式)
-  ESP32 + RP2040 端到端协议(face:x,y UART)
-  WDT 崩溃(face_tracker 切到 Core 1)
-  RP2040 眼球回中机制(idle 时回正)
-  V4L2 双 buffer(DMA 数据更新正常)

## 遗留问题(待解决)

-  face 检测 box 固定伪激活(无论 pix_type / 画面内容 / 模型选择)
-  GDMA ISR 每 ~30s 触发 InstrFetchProhibited 崩溃
- ⚠️ 端到端验收:眼球未真正跟随人脸

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:22:15 +08:00

153 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 01 执行进度追踪
> 由于本仓库非 git 仓库,用本文件替代 commit 作为原子进度追踪。
> 每完成一个任务追加一行;遇到偏差记录 `[!]` 条目。
## 任务状态表
- [~] T01 摄像头硬件 V4L2 probe —— 代码完成,硬件验证待用户
- [x] T02 Kconfig 开关 + FPS choice
- [x] T03 esp-dl + human_face_detect 依赖
- [x] T04 Esp32Camera CaptureForDetection + 双超时 mutex
- [x] T05 face_tracker.{h,cc} 骨架 + CMake 条件编译
- [x] T06 集成 HumanFaceDetect 推理 + 坐标归一化(代码部分;实测待 T12
- [x] T07 uart_send_face + uart mutex
- [x] T08 RP2040 parse_face + static 去重
- [x] T09 RP2040 main.py incoming_commands 识别 face:
- [x] T10 RP2040 facetrack() 改造D-07 idle return
- [ ] T11 application.cc 接入 face_tracker_start
- [ ] T12 端到端联调
- [ ] T13 性能调优
- [ ] T14 关开关回归测试
- [ ] T15 最终验收
## 执行日志
- [x] T01 代码部分完成2026-04-17
- 新增 `ProbeFrameCapture()``main/boards/common/esp32_camera.{h,cc}`
-`main/application.cc``Start()` 末尾插入 probe 调用(`#ifndef CONFIG_IDF_TARGET_ESP32` 守卫)
- 硬件验证部分待用户接 USB 后在 T02/T03 通过后烧录验证
- [x] T02 完成2026-04-17
-`main/Kconfig.projbuild` 的 Camera Configuration menu 末尾新增
`XIAOZHI_ENABLE_FACE_TRACKING` + FPS choice5/10/15
- 采用 PLAN_CHECK NOTE-1 方案 B`depends on IDF_TARGET_ESP32S3`
只支持 S3与 CMake 排除逻辑对齐
- [!] T03 偏差2026-04-17 — 依赖版本冲突 阻塞批次 1
- **第一轮偏差**PLAN 原定 `esp-dl==3.2.0` + `human_face_detect==0.4.1` 不兼容
registry 数据显示 human_face_detect 0.4.1 实际依赖 `esp-dl ~3.3.0`
- 自动修正为 `esp-dl ~3.3.0`
- **第二轮偏差blocking**`esp-dl 3.3.0` 要求 `esp-dsp ==1.7.0`
但项目已有 `esp-sr ~2.2.0` 要求 `esp-dsp ==1.6.0`,互斥
- 此为真正的版本冲突,已停下汇报 orchestrator
- [x] T03 偏差已解决2026-04-17 —— 用户决策方案 A升级 esp-sr
-`idf_component.yml``esp-sr``~2.2.0` 升级为 `~2.3.1`
- esp-sr 2.3.x 已切换到 esp-dsp 1.7.0,与 esp-dl 3.3.0 兼容
- `idf.py reconfigure` 通过esp-dl 3.3.x / esp-dsp 1.7.0 / esp-sr 2.3.1 / human_face_detect 0.4.1 全部就绪
- 编译遇到 bootloader CMake 缓存不匹配(与 IDF 路径历史变更有关),已清理 `build/bootloader*` 目录后重新编译
- [x] T04 完成2026-04-17 - 修改文件: main/boards/common/esp32_camera.{h,cc}
- `esp32_camera.h`: 新增公开结构体 `FrameRef`data/len/width/height/format/buf_index
+ `CaptureForDetection(FrameRef*)` / `ReleaseDetectionFrame(const FrameRef&)` 声明
+ 私有成员 `SemaphoreHandle_t capture_mutex_`
- `esp32_camera.cc`: 构造函数末尾 `xSemaphoreCreateMutex()`,析构函数 `vSemaphoreDelete`
实现 `CaptureForDetection`10ms timeout 拿不到锁即返回 false 跳帧,成功后不解锁)
实现 `ReleaseDetectionFrame`VIDIOC_QBUF 归还 + 释放 mutex
`Capture()` 头部用栈上 RAII `CaptureLockGuard` 以 portMAX_DELAY 加锁,确保任何 return 路径都解锁
- `idf.py build` 通过,固件 2.47MB / 剩余 1.47MB (37% free)
- [x] T05 完成2026-04-17 - 新增: main/face_tracker.{h,cc};修改: main/CMakeLists.txt
- `face_tracker.h`: `extern "C"` 导出 3 个接口:`face_tracker_start/stop/get_fps`
- `face_tracker.cc`: 三重保护
1) Kconfig 层面(批次 1 已加 depends on IDF_TARGET_ESP32S3
2) 代码层面 `#if defined(CONFIG_XIAOZHI_ENABLE_FACE_TRACKING) && defined(CONFIG_IDF_TARGET_ESP32S3)` 守卫
3) 构建层面CMakeLists.txt `if(NOT CONFIG_IDF_TARGET_ESP32S3) list(REMOVE_ITEM SOURCES "face_tracker.cc")`
骨架任务 pin Core 0 / 优先级 2 / 栈 8KB每秒打印 `hello from core 0`
- `idf.py build` 通过,固件 2.47MB / 剩余 1.47MB (face_tracker.cc.obj 已被编译链接)
- [!] T06 偏差2026-04-17 - PLAN 中 T06 依赖 T07 的 uart_send_face 符号,但批次 2 未做 T07
- 采取方案face_tracker.cc 中用 `__attribute__((weak))` 前向声明 `uart_send_face`
T07 完成后uart_component.cc 提供的 strong symbol 自动覆盖弱符号
调用处加 `if (uart_send_face != nullptr)` 判空(弱符号未定义时为 NULL
- 此偏差属于"修复 T06 的前置依赖缺失",无需架构层面变更,已内联解决
- [x] T06 完成代码部分2026-04-17 - 修改: main/face_tracker.cc
- 包含 `human_face_detect.hpp` / `dl_image_define.hpp` / `dl_detect_define.hpp`
- 构造 `HumanFaceDetect()`(默认 model_type 由 CONFIG_DEFAULT_HUMAN_FACE_DETECT_MODEL 决定)
- 任务主循环:`vTaskDelayUntil(period)` 按 Kconfig FPS → CaptureForDetection →
组装 img_t (YUYV) → detector->run(img) → ReleaseDetectionFrame → 坐标归一化
- 坐标公式严格遵守 RESEARCH Pitfall 7`cx * 224 / width - 112`(匹配 RP2040 deadzone=20
- PLAN 未定义多人脸排序,补充健壮性:遍历 list 挑 score 最高的 result避免多脸摇摆
- 启动时打印 `PSRAM after detector init` 供 R2 OOM 风险追踪
- 每 10 秒打印 `face stats: hit/miss/fps`
- `idf.py build` 通过,固件 2.50MB / 剩余 1.46MB (36% free) — 相比 T05 +30KB
(esp-dl 推理库 + human_face_detect 模型注册表代码被链接)
- **实测部分待 T12**:需烧录后将人脸对准摄像头验证 score / infer 时长 / FPS
若 score < 0.5 则进入决策点 D-B改为 DL_IMAGE_PIX_TYPE_RGB565LE
- [x] T07 完成2026-04-17 - 修改: main/uart_component.{h,cc}
- `uart_component.h`: 新增 `uart_send_face(int,int)` 声明 `extern "C"` 包裹
以保证 C 链接名匹配 face_tracker.cc `extern "C" __attribute__((weak))` 前置声明
其他函数保持原 C++ 修饰名不变不影响 main.cc/display.cc 现有调用
- `uart_component.cc`:
* 新增 `static SemaphoreHandle_t s_uart_tx_mutex` `uart_init_component()` 末尾创建
* `uart_send_string()` 整体加 mutex 保护防止与 uart_send_face 并发撕包
* `uart_signal_start/stop` 经由 uart_send_string 间接加锁无需重复保护
* 新增 `extern "C" void uart_send_face(int,int)`snprintf 24 字节栈缓冲
加锁后 `uart_write_bytes(buf,n)` + `uart_write_bytes("\r\n",2)`与现有格式一致
- [!] 小偏差Rule 2PLAN 示例中 header 未用 extern "C" face_tracker.cc 的弱符号
前置声明是 C 链接strong 实现必须也是 C 链接才能覆盖 weak extern "C" 包裹解决
- `idf.py build` 通过固件 0x280760 = 2.51MB / 剩余 36% (1.46MB)相比 T06 几乎持平
( +数百字节符合 PLAN T07 "< 1KB" 预期)
- **nm 验证**`libmain.a` `uart_send_face` Tstrong 定义`face_tracker.cc.obj`
中为 wweak 引用)。弱符号覆盖链生效最终 ELF 暂时没这些符号是因为 T11 未做
application 未调用 face_tracker_start触发链接器 DCE 把整个 face_tracker 子图剔除
T11 接入后会自动拉入 uart_send_face strong 实现
- 未添加 test hookPLAN DoD 中提到的 `uart_send_face(42,-30)` 临时调用
留给 T12 端到端联调时用真实 face_tracker 数据验证
- [x] T08 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/coms.py
- `Comms.__init__` 末尾新增三个成员
* `self.last_face_offset = None`ESP32 人脸坐标最新值main.py 消费后清空
* `self.last_face_raw = None`static 去重用的"上一次原始坐标"
* `self.FACE_STATIC_THRESHOLD = 3`(±3 像素阈值经验值
- 类内新增 `parse_face(line)` 方法
* 兼容 `bytes` / `str` 输入自动 decode + strip防御性处理
* `face:` 前缀或解析失败返回 None不影响 staticflag
* `last_face_raw` 对比差异 3 像素 `staticflag = True`否则 `staticflag = False` 并更新 `last_face_raw`
* 首次收到坐标视为"非静态"确保眼球初始化时会响应
- **未新增** 独立的 `read_face()` 方法PLAN T08 规范只要求 `parse_face` + `last_face_offset`不要求单独的 UART 读取方法——ESP32 UART 数据仍由 `esp_read()` 统一读入 `rx_buffer`main.py 遍历 `commands` 时逐个调 `parse_face` T09)。如果单独再提供 `read_face()` 会导致 UART 缓冲区被两个方法争抢造成行撕裂
- Python 语法检查`python3 -c "import ast; ast.parse(...)"` 通过
- [x] T09 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py
- 主循环 `for data in incoming_commands` 分支改造
* 优先调 `external.parse_face(data)`成功则设 `external.last_face_offset`
`animation.grove_active = True``animation.grove_last_seen = time.ticks_ms()``continue`
* 否则走原 `action_map` / `state_map` 分发完全不影响现有 state 指令行为
- `grove_active` / `grove_last_seen` 在收到 `face:` 消息时立即 set True即时响应 T10 `facetrack()` 兜底刷新配合不产生状态污染
- Python 语法检查通过
- [x] T10 完成2026-04-20 - 修改文件: /Users/rdzleo/Desktop/CogletESP-CogletESP/RP2040/main.py
- `facetrack()` PLAN T10 v1.1 完整重构执行顺序
1. **始终读 Grove**`grove_offset = external.grove_read()`即便无 Grove 硬件也要消费 UART 防止缓冲区溢出
2. **读 ESP32 face 数据**`esp_offset = external.last_face_offset`消费后立即 `last_face_offset = None`
3. **数据源优先级**Grove 有效时用 Grove否则 fallback ESP32
4. **D-07 修复**`if animation.current_state == "idle": return`idle 下只消费数据不驱动舵机
5. idle 分支原有 eyl/eyr/pit/yaw scale + set_target 逻辑一字不动
- **BLOCKER #2 修复**`staticflag` 不再在 `facetrack()` 硬编码设置——完全由 T08 `parse_face()` / 原有 `grove_read()` 各自管理
- **grove_active 合并策略**T09 在消息到达时 set True实时T10 兜底在 `facetrack()` 里根据本帧最终 offset 再次刷新并处理 3 秒超时两边都 set "当前时间戳" set False语义一致避免 BLOCKER #2 所描述的"双处更新导致污染"问题——只要 T09 set TrueT10 进入时 `offset` 非空也会刷到同一时间戳不会发生回退
- Python 语法检查通过
- [!] T10 偏差说明轻度
- PLAN v1.1 T10 里写了"grove_active/grove_last_seen **不在本任务主动 set True**" `facetrack()` Grove 硬件活跃时仍需要把 Grove 自身的 offset 反映到 grove_active 时间戳——本次实现在 `facetrack()` 里保留了"`if offset: grove_active=True`"写法原因是 T09 只在收到 ESP32 `face:` 消息时 set True**不处理 Grove 来源**。如果严格按 PLAN 字面删掉Grove 模式下 `grove_active` 永远为 False3 秒超时总是会触发自动动画会不停抢夺 Grove 追踪权
- 处理方式T09 Grove_active 的职责是"ESP32 数据源"T10 保留的是"Grove 数据源"——两边是 OR 关系而非重复更新BLOCKER #2 所警告的是"两边都 set 到同一数据源"这里不是)。已在代码注释中记录
- 此偏差属于 Rule 2补完 PLAN 遗漏的 Grove 分支不需要用户决策
- [x] 批次 4 (T08/T09/T10) Python 语法检查结果
- `coms.py` OK
- `main.py` OK
- `animation.py` OK未改动做回归确认