回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

FireBeetle 2 esp32c5+手势控灯 简单

头像 云天 2025.10.18 14 0

【项目背景】

本项目基于 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

image.png
修改代码中wifi ssid/password、mqtt_user/password (Iot_id/Iot_pwd)与主题(Topic),保持 Python 与 Arduino 一致。

【演示效果图】

f9cb857d1c991d460e4bde6084d231ea.jpg
4e26a5661fe5a6bc375c25c2781b81b4.jpg
eef5eb433a3504cb568569e41f846536.jpg
9874b699f2b246e247abc846927847a3.jpg
5abb6c4a9c8aeb195d218f2d8b011e35.jpg
0c60fc9c863b5eaccde0bc719d61bdec.jpg
bdf0791c2cff49bc69de41fe5f4f048b.jpg

【演示视频】

代码】

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()

因白天参加裁判工作,只能在晚上开工。

698f223c9b06e1b03dec1c99b87e5248.jpg

评论

user-avatar