# 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 choice（5/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 2）：PLAN 示例中 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` 为 T（strong 定义），`face_tracker.cc.obj`
    中为 w（weak 引用）。弱符号覆盖链生效。最终 ELF 暂时没这些符号是因为 T11 未做，
    application 未调用 face_tracker_start，触发链接器 DCE 把整个 face_tracker 子图剔除。
    T11 接入后会自动拉入 uart_send_face 的 strong 实现。
  - 未添加 test hook（PLAN 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 到 True，T10 进入时 `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` 永远为 False，3 秒超时总是会触发，自动动画会不停抢夺 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（未改动，做回归确认）

