实现 ESP32-S3 上单摄像头人脸追踪的核心代码骨架,替代 Grove Vision AI V2
模块,通过 UART 发送人脸坐标驱动 RP2040 控制的眼球/YAW 舵机。
## 规划文档(docs/phase-01-face-tracking/)
- GOAL.md Phase 目标与 5 大成功标准
- RESEARCH.md esp-dl v3.2/3.3 + human_face_detect 0.4.1 技术调研
- PLAN.md 15 个原子任务的执行计划(T01-T15)
- PLAN_CHECK.md 计划审查报告(PASS_WITH_NOTES)
- PROGRESS.md 执行进度追踪(批次 1-3 已完成)
## 批次 1:依赖与开关(T01-T03)
- main/idf_component.yml
新增 esp-dl ~3.3.0 + human_face_detect 0.4.1(仅 S3/P4)
esp-sr 从 ~2.2.0 升级到 ~2.3.1,解决 esp-dsp 1.6/1.7 版本冲突
- main/Kconfig.projbuild
新增 CONFIG_XIAOZHI_ENABLE_FACE_TRACKING 开关(默认 y,depends on S3)
新增 CONFIG_XIAOZHI_FACE_TRACKING_FPS_CHOICE(5/10/15)
- main/boards/common/esp32_camera.{h,cc}
新增 ProbeFrameCapture() 最小 V4L2 DQBUF/QBUF 探针(T01)
- main/application.cc
Start() 末尾调用 probe 验证摄像头硬件链路
## 批次 2:人脸检测核心(T04-T06)
- main/boards/common/esp32_camera.{h,cc}
新增 FrameRef 结构体 + CaptureForDetection/ReleaseDetectionFrame
双超时 mutex 策略:face_tracker 10ms timeout 跳帧,Capture() RAII guard
- main/face_tracker.{h,cc}(新建)
Core 0 / 优先级 2 / 栈 8KB 独立任务
集成 esp-dl HumanFaceDetect 推理
坐标归一化 cx*224/W-112,匹配 RP2040 pixel_centre=112
多人脸遍历挑 score 最高,避免多脸时眼球摇摆
三重保护:Kconfig depends on S3 + 源文件 #if 守卫 + CMake 条件排除
- main/CMakeLists.txt
非 S3 目标从 SOURCES 移除 face_tracker.cc
## 批次 3:UART 协议扩展(T07)
- main/uart_component.{h,cc}
新增 uart_send_face(x,y) 发送 face:x,y\r\n 协议
extern "C" 链接名配合 face_tracker 的弱符号声明
全局 TX mutex 保护所有 UART 写入,防并发帧交织
uart_send_string 同步加锁保持一致性
## 编译验证
idf.py build 通过,固件 2.51MB / 剩余 1.46MB (36% free)
当前 face_tracker 未被 application 激活(留到 T11),
UART/摄像头现有功能零影响。
## 未完成(下次继续)
- T01 硬件 probe 实机验证
- T08-T10 RP2040 端 parse_face + facetrack 双数据源改造
- T11-T15 application 接入 + 端到端联调 + 性能调优 + 最终验收
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
C++
76 lines
2.9 KiB
C++
#include "uart_component.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#include "freertos/semphr.h"
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
|
||
// T07: UART TX 全局互斥锁
|
||
// 保护所有 uart_write_bytes 调用,防止 face_tracker 任务与 application 任务并发
|
||
// 写入造成帧交织(RESEARCH A3)
|
||
static SemaphoreHandle_t s_uart_tx_mutex = nullptr;
|
||
|
||
// 初始化 ESP32 → RP2040 的 UART 通信
|
||
// 波特率 115200,8 数据位,无校验,1 停止位,无流控
|
||
void uart_init_component() {
|
||
uart_config_t uart_config = {
|
||
.baud_rate = 115200,
|
||
.data_bits = UART_DATA_8_BITS,
|
||
.parity = UART_PARITY_DISABLE,
|
||
.stop_bits = UART_STOP_BITS_1,
|
||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||
};
|
||
uart_param_config(UART_PORT_NUM, &uart_config);
|
||
// GPIO17=TX(发送到 RP2040 的 GP5/RX),GPIO18=RX(接收 RP2040 的 GP4/TX)
|
||
uart_set_pin(UART_PORT_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||
uart_driver_install(UART_PORT_NUM, BUF_SIZE, 0, 0, NULL, 0);
|
||
|
||
// T07: 创建 TX 全局互斥锁
|
||
if (s_uart_tx_mutex == nullptr) {
|
||
s_uart_tx_mutex = xSemaphoreCreateMutex();
|
||
}
|
||
}
|
||
|
||
// 发送状态字符串给 RP2040,末尾自动添加 \r\n
|
||
// RP2040 的 main.py 通过 coms.esp_read() 按 \n 分割解析
|
||
// 支持的状态字符串:idle / listening / speaking / thinking / neutral / happy 等
|
||
// T07: 加锁,防与 uart_send_face 并发撕包
|
||
void uart_send_string(const char* str) {
|
||
if (s_uart_tx_mutex != nullptr) {
|
||
xSemaphoreTake(s_uart_tx_mutex, portMAX_DELAY);
|
||
}
|
||
uart_write_bytes(UART_PORT_NUM, str, strlen(str));
|
||
uart_write_bytes(UART_PORT_NUM, "\r\n", 2);
|
||
if (s_uart_tx_mutex != nullptr) {
|
||
xSemaphoreGive(s_uart_tx_mutex);
|
||
}
|
||
}
|
||
|
||
// 发送说话开始信号(预留接口,RP2040 当前未使用)
|
||
// 注意:经由 uart_send_string 间接加锁
|
||
void uart_signal_start() {
|
||
uart_send_string("[SPEAK_START]\n");
|
||
}
|
||
|
||
// 发送说话停止信号(预留接口,RP2040 当前未使用)
|
||
// 注意:经由 uart_send_string 间接加锁
|
||
void uart_signal_stop() {
|
||
uart_send_string("[SPEAK_STOP]\n");
|
||
}
|
||
|
||
// T07: 发送人脸检测坐标到 RP2040
|
||
// 格式:"face:<x>,<y>\r\n",x/y ∈ [-112, +112](RP2040 pixel_centre=112)
|
||
// 由 face_tracker 任务以 Kconfig FPS 频率调用(默认 10 FPS)
|
||
// 必须是 C 链接(extern "C")——face_tracker.cc 用 weak 符号前置声明,
|
||
// 链接时本 strong 实现自动覆盖 weak。
|
||
extern "C" void uart_send_face(int x_offset, int y_offset) {
|
||
if (s_uart_tx_mutex == nullptr) return; // UART 未初始化,直接丢弃
|
||
char buf[24];
|
||
int n = snprintf(buf, sizeof(buf), "face:%d,%d", x_offset, y_offset);
|
||
if (n <= 0 || n >= (int)sizeof(buf)) return; // 格式化失败/截断
|
||
xSemaphoreTake(s_uart_tx_mutex, portMAX_DELAY);
|
||
uart_write_bytes(UART_PORT_NUM, buf, n);
|
||
uart_write_bytes(UART_PORT_NUM, "\r\n", 2);
|
||
xSemaphoreGive(s_uart_tx_mutex);
|
||
}
|