# Phase 1 PLAN.md 第二轮审查报告

**审查日期:** 2026-04-17
**审查对象:** `/Users/rdzleo/Desktop/CogletESP-camera-version/docs/phase-01-face-tracking/PLAN.md` v1.1
**审查方法:** 对照第一轮 PLAN_CHECK.md 每条问题逐一核验 + 实际代码交叉
**审查人:** GSD Plan Checker（第二轮）

---

## 1. 审查结论

**`PASS_WITH_NOTES`**

第一轮提出的 3 个 BLOCKER + 3 个 HIGH 全部得到正确修复，修复质量整体良好。Revision History 完整、决策点 D-07 在"用户已决策"表中明确登记、依赖图笔误已订正、新增风险 R13/R14/R15 与代码逻辑一致。**可以进入执行阶段**。

仅有 2 处轻微注意点（不阻塞，仅作提醒），见第 3 节。

---

## 2. 原问题修复情况表

| 编号 | 严重级 | 原问题 | 是否修复 | 修复质量 | 备注 |
|------|--------|--------|----------|----------|------|
| BLOCKER #1 | 🔴 | T10 facetrack 缺失 idle 判断 | ✅ 已修复 | **好** | T10 修订（L667-734）+ D-07 决策（L48）+ R15 风险登记（L1078） |
| BLOCKER #2 | 🔴 | grove_active 重复更新 | ✅ 已修复 | **好** | T10 删除重复更新代码 + T09（L659）注释明确"只在此处更新" |
| BLOCKER #3 | 🔴 | T01 用 Capture() 过重 | ✅ 已修复 | **好** | T01 重写为最小 V4L2 DQBUF/QBUF probe，预算 < 200ms |
| HIGH #1 | 🟠 | CMakeLists 非 S3 目标 | ✅ 已修复 | **好** | 三重保护：Kconfig depends + CMake REMOVE_ITEM + 源文件 #if 守卫 |
| HIGH #2 | 🟠 | Capture mutex 饥饿 | ✅ 已修复 | **好** | T04 双超时策略（detection 10ms timeout 跳帧，capture portMAX_DELAY） |
| HIGH #3 | 🟠 | staticflag 硬编码 False 漂移 | ✅ 已修复 | **好** | T08 新增 `last_face_raw` + `FACE_STATIC_THRESHOLD=3` 去重逻辑 |
| 笔误 | — | 依赖图 T05→T07 | ✅ 已修复 | **好** | 改为 T06→T07，并在 v1.1 标头明确说明 |

---

## 3. 修复细节核验

### BLOCKER #1（T10 idle 判断）

- **D-07 决策已登记**（L48）：明确"idle 状态下 RP2040 不驱动眼球舵机追踪。ESP32 侧行为不变（按 D-03 始终发送坐标）；RP2040 侧收到 face:x,y 时，若 animation.current_state == 'idle'，仅更新 grove_active/grove_last_seen/last_face_offset 状态，不调用 set_target()"
- **代码位置正确**（L699-702）：idle 判断放在数据消费 + grove_active 超时之后、舵机驱动之前——这是**严格正确的位置**：
  - 先消费 `last_face_offset`（避免数据堆积）✅
  - 再做 3s 超时回退（即便 idle 也要正确清 `grove_active`）✅
  - 最后 `if idle: return`，跳过 servo 驱动 ✅
- **对齐原代码语义**：原 `facetrack()` L53 是 `if animation.current_state != "idle":`，T10 用 `if idle: return` 实现等价转换，且把 `grove_active` 超时判定上移到 idle return 之前——这反而比原代码更稳健（原代码 idle 时根本不更新 grove_active）。
- **DoD 测试用例完整**（L717-730）：手动测试 1（非 idle 追踪）、测试 2（idle 不追踪）、测试 3（3s 超时）、测试 4（staticflag 联动）四项覆盖。

### BLOCKER #2（grove_active 重复更新）

- **T09 单一更新点**（L649-651）：`animation.grove_active = True; animation.grove_last_seen = time.ticks_ms()` 只在 incoming_commands 循环里设置。
- **T09 显式注释**（L659）："grove_active / grove_last_seen 的更新**只在此处**做，T10 的 facetrack() 不再重复更新"——意图清晰。
- **T10 完全删除原 L456-461 重复代码**：T10 修订后只保留"3 秒无数据回退"兜底（L694-697），不再 set True。
- **职责划分清晰**：
  - T09：incoming_commands 收到 face: → set True + 更新 last_seen
  - T10：facetrack 检查超时 → set False
  - 两处不冲突，未来 `grove_last_seen` 真正反映"最后一次收到 ESP32 数据的时间"。

### BLOCKER #3（T01 最小 probe）

- **改为 V4L2 直接 ioctl**（L75-99）：只调一次 `VIDIOC_DQBUF` + `VIDIOC_QBUF`，不触发 JPEG 编码、不做 PSRAM 大分配、不触发 encoder_thread。
- **执行时间预算合理**：DoD（L116）要求 `elapsed < 200ms`——这是合理的（V4L2 DQBUF 在 10 FPS 下理论 < 100ms 就能拿到下一帧；DVP 唤醒延迟也在此预算内）。
- **probe 调用位置正确**（L101-112）：放在 `Application::Start()` 末尾、`protocol_->Start()` 之后——此时 `Esp32Camera` 已构造完成，`streaming_on_=true`，video_fd_ 已 open。
- **保留诊断 API**（L114）：T04 完成后只删 probe 调用、保留 `ProbeFrameCapture` API 作为诊断工具——比原方案"完全删除"更稳。

### HIGH #1（CMakeLists 非 S3）

- **三重保护明确写入** T05（L370-387 + L388-394 兼容性表）：
  1. **Kconfig 层**：`depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`（T02 L139）
  2. **CMake 层**：`if(NOT CONFIG_IDF_TARGET_ESP32S3) list(REMOVE_ITEM SOURCES "face_tracker.cc") endif()`（T05 L380-382）
  3. **源文件层**：`#if defined(CONFIG_XIAOZHI_ENABLE_FACE_TRACKING) && defined(CONFIG_IDF_TARGET_ESP32S3)` 包裹所有 esp-dl include（T05 L319）
- **兼容性表**（L388-394）覆盖 ESP32-S3 / P4 / 原版 / C3 / C6 五种目标的预期行为。
- **DoD 加入交叉验证**（L397）："ESP32（原版）编译通过（验证非 S3 目标不会因 human_face_detect.hpp 缺失而失败）"。
- **轻微注意**：T05 L392 写"ESP32-P4：face_tracker.cc 编译为空壳"——但 T02 Kconfig L139 明确 `depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`，且 T05 L380 CMake 条件是 `if(NOT CONFIG_IDF_TARGET_ESP32S3)` —— 这意味着 P4 目标会被 CMake 排除掉编译（与 Kconfig 允许冲突）。**详见第 4 节 NOTE-1**。

### HIGH #2（Capture mutex 饥饿）

- **T04 双超时策略明确**（L209，修订说明）："face_track 拿不到锁就跳过这一帧（人脸检测允许丢帧），拍照则可完整持有 mutex"。
- **代码层面正确**：
  - `CaptureForDetection`（L240）：`xSemaphoreTake(capture_mutex_, pdMS_TO_TICKS(10))`，拿不到立即返回 false
  - `Capture`（L275）：`xSemaphoreTake(capture_mutex_, portMAX_DELAY)` 等任意时长
- **使用 FreeRTOS Semaphore 而非 std::mutex**：注释（L231）解释了原因（std::mutex 的 `try_lock_for` 在 ESP32 toolchain 上不可移植）——**正确的工程权衡**。
- **新增 R13 风险登记**（L1076）：与代码逻辑完全一致。
- **T12 步骤 7 显式验证**（L796-799）：listening 状态下触发 MCP take_photo，观察 face_tracker 跳帧而非卡死。
- **轻微注意**：`CaptureForDetection` 与 `ReleaseDetectionFrame` 的 mutex 是**跨调用持有**——`CaptureForDetection` 拿锁后不解锁，由后续的 `ReleaseDetectionFrame` 解锁。这是正确的（保护 mmap_buffers_[buf.index] 的内容直到 caller 用完），但 face_tracker_task 必须保证两者**严格成对**调用。**详见第 4 节 NOTE-2**。

### HIGH #3（staticflag 漂移）

- **T08 新增 static 去重逻辑**（L592-606）：
  - 与 `last_face_raw` 比较 dx, dy
  - 阈值 ≤ `FACE_STATIC_THRESHOLD=3` → 设 `staticflag = True`
  - 否则更新 `last_face_raw` + 设 `staticflag = False`
  - 首次收到坐标视为非静态（合理）
- **阈值 3 像素的合理性**：
  - 在 224×224 归一化坐标下约 2.7%
  - 对照 `coms.py` L60 `deadzone = 20`（更宽容），3 像素只用于**消除 bbox 抖动**，不会影响 deadzone 判定
  - 与原 Grove 的 `if boxes_part != self.last_boxes` 字符串完全相等比较（coms.py L80）相比，3 像素阈值更宽容（容忍 bbox 抖动），是**合理升级**
- **T10 不再硬编码 staticflag = False**（L686 注释明确）："注意：staticflag 已在 T08 parse_face 中更新，此处不再设置"
- **DoD 完整覆盖**（L618-628）：基本解析 4 条 + static 去重 4 条断言。
- **R14 风险登记 + D-D 决策点**（L1077, L989-992）：留出实测调校空间。

### 依赖图笔误

- **L900-902 已改为 T06→T07**：图中 T07 的箭头指向 T06，配上注释 "← T06 依赖 T07（v1.1 修正）"
- **v1.1 修订说明明确**（L20, L873-875）："原版图中 T05→T07 是笔误。实际 T05 只是骨架任务（打印 hello），不调用 uart_send_face；是 T06 才调用 uart_send_face 完成坐标推送"
- **执行顺序表同步更新**（L946）："T06 前置：T02、T03、T04、T05、T07 全部完成后才能开工（v1.1 修正）"

---

## 4. 新发现的问题

### 🔵 NOTE（提示性，不阻塞执行）

#### NOTE-1: ESP32-P4 在 Kconfig 允许但 CMake 排除——存在矛盾

- **位置:** T02 L139（Kconfig `depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32P4`） vs T05 L380（CMake `if(NOT CONFIG_IDF_TARGET_ESP32S3)`）
- **问题:** Kconfig 允许 P4 看到选项并打开，但 CMake 在 P4 上会移除 `face_tracker.cc`，导致 face_tracker 函数变成空壳（`#else` 分支的 3 个空函数）。如果用户在 P4 上启用该选项，编译能过但功能不工作——日志里也不会有任何报错。
- **建议:** 二选一：
  - **方案 A**：CMake 改为 `if(NOT CONFIG_IDF_TARGET_ESP32S3 AND NOT CONFIG_IDF_TARGET_ESP32P4)` 一并支持 P4
  - **方案 B**：Kconfig 改为 `depends on IDF_TARGET_ESP32S3` 只支持 S3，把 P4 从声明列表中去掉
- **不阻塞**：本项目硬件是 S3-N16R8，P4 路径根本不会被走到。但作为面向其他用户的 Kconfig 开关，最好和实际 CMake 行为对齐。

#### NOTE-2: `CaptureForDetection` 与 `ReleaseDetectionFrame` 跨调用持锁——face_tracker 任务必须严格成对

- **位置:** T04 L256-258（注释"不解锁！由 ReleaseDetectionFrame 配对解锁"），T06 L443/458 调用配对
- **观察:** T06 的实现已经正确做到了：在 `CaptureForDetection` 返回 true 后立即用 `auto& results = detector->run(img)`，然后立刻调用 `ReleaseDetectionFrame(f)`。中间没有 `continue`/`return`/异常路径会跳过 `ReleaseDetectionFrame`。
- **潜在隐患:** 如果未来有人在 T06 的 if-else 分支中插入 early return（例如某种检测失败的快速路径），可能漏掉 `ReleaseDetectionFrame` → 整个 capture mutex 永久持有 → MCP 拍照永久卡死。
- **建议:** T06 的 ReleaseDetectionFrame 调用建议用 RAII 风格包装：
  ```cpp
  // 可选改进：建一个 helper struct
  struct DetectionFrameGuard {
      Esp32Camera* cam;
      Esp32Camera::FrameRef* f;
      ~DetectionFrameGuard() { if (cam && f) cam->ReleaseDetectionFrame(*f); }
  };
  ```
  或者至少在 T06 的代码注释中加 **"绝对不要在 CaptureForDetection true 之后到 ReleaseDetectionFrame 之间插入 early return"** 的警示。
- **不阻塞**：当前代码路径正确，仅作未来维护提醒。

---

## 5. 已修复但值得点赞的设计

1. **T10 idle return 之前先做 3 秒超时回退**（L694-702）——比原 `facetrack()` 在 idle 下完全不更新 grove_active 的行为**更稳健**。这意味着用户在 idle 下离开摄像头 3 秒后再唤醒到 listening，`grove_active=False` 已被正确清掉，不会出现"基于过期数据驱动眼球"的视觉异常。
2. **T01 保留 `ProbeFrameCapture` 作为诊断 API**（L114）——比"完全删除"更工程化，未来 issue 排查可独立触发。
3. **T05 三重保护**（Kconfig + CMake + 源文件 #if）——任一机制失效另两层兜底，鲁棒性极高。
4. **T08 阈值 3 像素的工程经验值**——对应 coms.py L60 `deadzone=20`，比例合理；而且在 DoD 里给了完整断言（包括边界 = 阈值的 case）。
5. **R13/R14/R15 与 D-D/D-07 双向链接**——风险登记和决策点形成闭环。

---

## 6. Revision History 完整性核验

- ✅ Revision History 表（L13-16）清晰列出 v1.0 → v1.1 的变更摘要
- ✅ "v1.1 涉及修改的任务"明确列举（L18）：T01、T04、T06、T08、T10
  - 注：T05/T07/T09 也有少量修订（CMake/注释），但属于配套调整，未单独标记
  - **轻微遗漏**：T05 实际有修订（CMake 条件编译策略，HIGH #1 修复），但 v1.1 摘要里没列入。建议下次修订时补全。
- ✅ "v1.1 新增决策"（L19）明确列出 D-07
- ✅ "v1.1 依赖图修正"（L20）明确指出 T06→T07
- ✅ 每个修订任务都有 `[修订于 2026-04-17]` 标记：T01（L63）、T04（L206）、T05（L287）、T08（L549）、T10（L667）—— **T06 没有**[修订于 2026-04-17] 标记，但实际 T06 内容相对 v1.0 有微调（FPS 计算加保底防除零）。建议补上。
- ✅ 任务编号保持 T01-T15 不变
- ✅ 任务清单快速参考（L1148-1166）的 v1.1 修订列已正确标注

---

## 7. 推荐下一步

### ✅ 批准进入执行阶段

第一轮的 3 BLOCKER + 3 HIGH 已全部修复且修复质量良好；新增风险 R13/R14/R15 与代码逻辑一致；Revision History 基本完整；依赖图笔误已订正。

**可以执行 `/gsd-execute-phase 01-face-tracking`**。

### 执行过程中建议留意

1. **NOTE-1（P4 行为不一致）**：执行 T02/T05 时如果方便，顺手统一 Kconfig 和 CMake 的目标列表（建议改 Kconfig 为 `depends on IDF_TARGET_ESP32S3` 只支持 S3，因为本项目硬件就是 S3）。
2. **NOTE-2（CaptureForDetection RAII）**：执行 T04/T06 时考虑加 RAII guard 或至少加警示注释，避免未来维护引入 mutex 永久持有的 bug。
3. **R14（FACE_STATIC_THRESHOLD 调校）**：T12 阶段必须实测验证 3 像素阈值是否合适，准备好按 D-D 调整。
4. **D-07 用户体验观察**：T12 步骤 3 显式测试 idle 状态——这是本次修订的核心，务必拍视频/截图记录眼球闭眼且不动的状态作为 ACCEPTANCE.md 证据。

### 不阻塞但可优化（执行后再议）

- 给 T05 / T06 补 [修订于 2026-04-17] 标记（Revision History 完整性）
- T05 兼容性表对 P4 行为表述对齐 NOTE-1 的修订

---

## 8. 审查总结

- **正面：** 第一轮提出的所有问题都得到准确理解和修复；修复方案不仅"满足要求"还体现了工程权衡（如 FreeRTOS Semaphore vs std::mutex、RAII 担心、保留诊断 API）；新增决策 D-07 的设计深入到了"idle 时眼睑闭合所以追踪无意义"的产品语义层面，不是机械修复。
- **负面：** Revision History 标记不完全（T05/T06 缺标记），少量 Kconfig/CMake 目标列表不一致——但都属于轻微注意，不影响执行。
- **结论：** 修订质量超出预期，**PASS_WITH_NOTES**，进入执行阶段。
