【项目背景】
本项目基于 DFRobot FireBeetle 2 ESP32-C6 与 5 颗 NeoPixel 灯珠,通过 MediaPipe 实时识别手掌 5 根手指的屈伸状态,再用 MQTT(Easy IOT 平台)把“0/1”信号传到开发板,实现“哪根手指伸直,哪颗 LED 就亮绿;弯曲则亮红”的直观交互。
【整体架构】
手掌 → 摄像头 → Python(MediaPipe 识别) → MQTT 消息 → ESP32-C6(订阅)→ NeoPixel 灯珠
感知端:OpenCV + MediaPipe 检测 21 个手部关键点,对比指尖与掌根/第二关节的坐标,判断“伸直/弯曲”。
通信端:使用 Easy IOT(或任意 MQTT Broker)把 5 位二进制字符串(如 10110)发到主题 z4ksqL6Ig。
执行端:FireBeetle ESP32-C6 订阅该主题,解析后直接驱动 Adafruit_NeoPixel 库,5 颗灯珠一对一映射 5 根手指。
【Arduino 端代码精要】
(已附完整源码,这里只讲 3 个关键点)
双效果模式
currentEffect=0 为流水灯,用于“待机提示”;一旦收到 MQTT 消息立即切换到 currentEffect=1,进入手势实时模式。
非阻塞动画
runningLights() 用 millis() 做时间片,避免 delay() 卡死 MQTT 心跳。
零拷贝解析
收到 payload 后直接 message.charAt(i) 与 '1' 比较
【Python 端代码精要】
MediaPipe 参数
静态图模式关闭,只做单手检测,置信度 0.5 即可,笔记本 720 p 帧率可达 40 FPS。
拇指单独判断
拇指自由度大,代码用掌根方向动态判断“相对伸直”,其余四指统一比较 tip_y < pip_y。
变化触发
只有当前帧与前一帧状态不同才发布 MQTT,减少 90 % 流量。
【环境配置】
配置Arduino IDE
添加ESP32-C5支持:
打开首选项,添加开发板管理器网址(http://172.104.52.16/package_esp32_dev_index_cn.json)
安装ESP32开发板支持包(搜索ESP32,点击安装3.3.0-alpha1)
选择ESP32-C5开发板
安装所需库:
Adafruit NeoPixel库
配置Python
pip install opencv-python mediapipe siot
配置Easy IOT
修改代码中wifi ssid/password、mqtt_user/password (Iot_id/Iot_pwd)与主题(Topic),保持 Python 与 Arduino 一致。
【演示效果图】







【演示视频】
【代码】
Arduino c(FireBeetle 2 esp32c5)代码:
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Adafruit_NeoPixel.h>
#define PIN 3
#define NUMPIXELS 5
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
// WiFi配置
const char* ssid = "***";
const char* password = "*********";
// MQTT配置
const char* mqtt_server = "iot.dfrobot.com.cn"; // 替换为你的IoT服务器
const int mqtt_port = 1883;
const char* mqtt_user = "*****";
const char* mqtt_password = "********";
const char* subscribe_topic = "z4ksqL6Ig"; // 订阅手指状态主题
WiFiClient espClient;
PubSubClient client(espClient);
// 效果控制变量
int currentEffect = 0;
unsigned long previousMillis = 0;
int effectPosition = 0;
String currentColor = "0,255,0"; // 默认绿色
// 手指状态存储
bool fingerStates[5] = {false, false, false, false, false}; // 拇指,食指,中指,无名指,小指
String fingerNames[5] = {"Thumb", "Index", "Middle", "Ring", "Pinky"};
void setup() {
Serial.begin(115200);
pixels.begin();
pixels.show(); // 初始化时关闭所有灯
// 连接WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi已连接");
Serial.print("IP地址: ");
Serial.println(WiFi.localIP());
// 设置MQTT
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mqttCallback);
connectMQTT();
}
void loop() {
// 保持MQTT连接
if (!client.connected()) {
connectMQTT();
}
client.loop();
// 根据当前效果执行动画
switch(currentEffect) {
case 0: // 流水灯效果
runningLights();
break;
case 1: // 手势识别控制
handcontrol();
break;
}
delay(10);
}
// ========== MQTT连接函数 ==========
void connectMQTT() {
while (!client.connected()) {
Serial.print("尝试连接MQTT...");
String clientId = "ESP32-FingerControl";
if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
Serial.println("MQTT连接成功");
client.subscribe(subscribe_topic);
Serial.println("已订阅主题: " + String(subscribe_topic));
} else {
Serial.print("MQTT连接失败, rc=");
Serial.print(client.state());
Serial.println(" 5秒后重试...");
delay(5000);
}
}
}
// ========== MQTT回调函数 ==========
void mqttCallback(char* topic, byte* payload, unsigned int length) {
Serial.print("收到消息 [");
Serial.print(topic);
Serial.print("]: ");
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.println(message);
// 解析手指状态
if (message.length() == 5) {
for (int i = 0; i < 5; i++) {
fingerStates[i] = (message.charAt(i) == '1');
}
// 切换到手势控制模式
currentEffect = 1;
Serial.println("手指状态更新:");
for (int i = 0; i < 5; i++) {
Serial.println(fingerNames[i] + ": " + (fingerStates[i] ? "伸直" : "弯曲"));
}
}
}
// ========== 灯光效果函数 ==========
// 流水灯效果
void runningLights() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= 150) {
previousMillis = currentMillis;
// 清除所有灯
for(int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
// 设置当前灯和下一个灯
int current = effectPosition % NUMPIXELS;
int next = (effectPosition + 1) % NUMPIXELS;
pixels.setPixelColor(current, pixels.Color(0, 150, 0)); // 主灯绿色
pixels.setPixelColor(next, pixels.Color(0, 50, 0)); // 下一个灯暗绿色
pixels.show();
effectPosition = (effectPosition + 1) % NUMPIXELS;
}
}
// 手势控制灯光效果
void handcontrol() {
// 根据手指状态控制对应的LED灯
for(int i = 0; i < NUMPIXELS; i++) {
if (fingerStates[i]) {
// 手指伸直 - 亮绿灯
pixels.setPixelColor(i, pixels.Color(0, 255, 0));
} else {
// 手指弯曲 - 亮红灯
pixels.setPixelColor(i, pixels.Color(255, 0, 0));
}
}
pixels.show();
// 可选:添加延时避免频繁更新
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 100) {
lastUpdate = millis();
// 可以在这里添加串口调试信息
Serial.print("手势控制模式 - 灯状态: ");
for(int i = 0; i < NUMPIXELS; i++) {
Serial.print(fingerStates[i] ? "1" : "0");
}
Serial.println();
}
}
python代码(电脑)
import cv2
import mediapipe as mp
import siot
# 主程序开始
siot.init(client_id="5245239075444325", server="iot.dfrobot.com.cn", port=1883, user="********", password="********")
siot.connect()
siot.loop()
# 初始化MediaPipe手部模型
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils
hands = mp_hands.Hands(static_image_mode=False,
max_num_hands=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5)
# 打开摄像头
cap = cv2.VideoCapture(0)
# 存储上一帧的手指状态,用于检测变化
previous_finger_state = [False, False, False, False, False] # 拇指,食指,中指,无名指,小指
finger_names = ['Thumb', 'Index', 'Middle', 'Ring', 'Pinky']
# 手指状态编码
# 0:弯曲, 1:伸直
def send_finger_status(current_state):
"""发送手指状态到SIoT"""
# 将手指状态编码为字符串,例如 "01001" 表示只有食指和小指伸直
status_code = ''.join(['1' if state else '0' for state in current_state])
# 发送编码后的状态
siot.publish(topic="z4ksqL6Ig", data=status_code)
# 同时发送可读的状态描述
extended_fingers = [finger_names[i] for i, state in enumerate(current_state) if state]
status_description = f"Extended: {', '.join(extended_fingers)}" if extended_fingers else "All fingers bent"
siot.publish(topic="z4ksqL6Ig_description", data=status_description)
print(f"Sent finger status: {status_code} - {status_description}")
while True:
ret, frame = cap.read()
if not ret:
break
# 水平翻转帧,使体验更直观
frame = cv2.flip(frame, 1)
# MediaPipe需要RGB格式的图像
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 处理帧,检测手部
results = hands.process(rgb_frame)
# 当前帧的手指状态
current_finger_state = [False, False, False, False, False]
finger_count = 0
if results.multi_hand_landmarks:
for hand_landmarks in results.multi_hand_landmarks:
# 绘制手部关键点和连接线
mp_drawing.draw_landmarks(frame, hand_landmarks, mp_hands.HAND_CONNECTIONS)
# 获取手腕关键点作为参考点
wrist_x = hand_landmarks.landmark[0].x
wrist_y = hand_landmarks.landmark[0].y
# 定义指尖关键点和其对应的第二关节关键点
finger_tips = [4, 8, 12, 16, 20]
finger_pips = [2, 6, 10, 14, 18] # 第二关节
for i, (tip, pip) in enumerate(zip(finger_tips, finger_pips)):
tip_x = hand_landmarks.landmark[tip].x
tip_y = hand_landmarks.landmark[tip].y
pip_x = hand_landmarks.landmark[pip].x
pip_y = hand_landmarks.landmark[pip].y
# 判断手指是否伸直
if i == 0: # 拇指
is_extended = (tip_x < pip_x) if hand_landmarks.landmark[5].x < wrist_x else (tip_x > pip_x)
else: # 其他四指
is_extended = tip_y < pip_y
current_finger_state[i] = is_extended
if is_extended:
finger_count += 1
# 检查手指状态是否有变化
if current_finger_state != previous_finger_state:
send_finger_status(current_finger_state)
previous_finger_state = current_finger_state.copy()
# 在图像上显示结果
cv2.putText(frame, f"Count: {finger_count}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
# 显示每个手指的状态
status_text = ""
for i, name in enumerate(finger_names):
status = "Extended" if current_finger_state[i] else "Bent"
color = (0, 255, 0) if current_finger_state[i] else (0, 0, 255)
y_position = 70 + i * 25
cv2.putText(frame, f"{name}: {status}", (10, y_position),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
cv2.imshow('Finger Count', frame)
if cv2.waitKey(1) & 0xFF == 27: # 按ESC退出
break
cap.release()
cv2.destroyAllWindows()
因白天参加裁判工作,只能在晚上开工。

评论