Community project
Gesture-Activated Iron Man Repulsor
Build a wearable gesture controller that lights up a 16-LED ring on your wrist when you make a fist and hold your hand at an angle. The ESP32 reads motion data from an MPU6050 accelerometer mounted on the back of your hand, detects the gesture, and fades the LEDs in and out with smooth animations. Optional color-cycling mode activates with a quick twist.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the sensor and LED ring to your hand, building a regulated power supply on your wrist, and uploading the gesture-detection firmware. You'll learn how to calibrate the motion sensor, tune gesture sensitivity, and safely power the system from a rechargeable USB battery.
Wiring diagram

Gather all the parts
Assemble it in 12 steps
1. Preparar las piezas correctas
Comprar ESP32-S3-DevKitC-1 N8 oficial (micro-USB), no una copia USB-C sin esquema. La batería usa cable USB-C a salida de alimentación polarizada de 5 V; para programar, cable de datos micro-USB. Sensor Adafruit #3886, no GY-521. Anillo comercial WS2812B de 16 LED: confirmar diámetro 68 mm y DIN.
2. Construir la alimentación en la muñeca
Sin batería conectada, llevar +5 V al fusible 1.5 A, primer polo del interruptor, ánodo SS34 y cátodo (banda) al bus 5 V. Retorno de batería al segundo polo y de allí a GND común. Llevar bus 5 V al pin 5V del ESP32, anillo y buffer. Llevar GND a los tres. No conectar una segunda alimentación USB al ESP32 al mismo tiempo.
- No llevar puesto durante las primeras pruebas. La batería debe limitar cortocircuitos. El fusible no sustituye aislamiento.
3. Montar el adaptador de datos
SN74AHCT125 DIP mirando desde arriba, muesca arriba: pin 1 arriba izquierda, numeración antihoraria. 14 a 5 V, 7 a GND; 1 a GND, 2 a GPIO14, 3 a resistencia 330 ohm y de allí DIN. 100 kohm de pin 2 a GND. Pines 4,10,13 a 5 V; 5,9,12 a GND. 6,8,11 libres. Cerámico 100 nF entre 14 y 7, junto al chip. No sustituir AHCT por HC.
4. Filtrar la alimentación del núcleo
1000 uF junto al anillo: positivo a 5 V, negativo marcado con franja a GND. DOUT sin conectar. Las resistencias de corriente de cada LED están integradas en WS2812B; la resistencia externa 330 ohm protege la señal, no alimenta los LED.
5. Conectar el sensor en la mano
VIN a 3V3 del ESP32, GND común, SDA a GPIO8 (datos), SCL a GPIO9 (reloj), AD0 a GND. INT, 3Vo y conexiones auxiliares libres. Mantener cables I2C cortos, idealmente menos de 20 cm.
6. Verificar en mesa antes de conectar placas
Comprobar ausencia de cortos con multímetro y polaridad; conectar batería solo al circuito de potencia y medir. Después conectar placa, buffer y sensor sin anillo; verificar 3.3 V y comunicación. Finalmente conectar anillo sin tensión y probar. Medir bus protegido entre 4.5 y 5.25 V bajo carga; si baja, usar cable más corto/grueso y otra batería, no omitir protección.
7. Probar gestos y seguridad
Al encender, mano quieta en reposo 4 segundos. Inclinar más de 65 grados y mantener 650 ms: rampa azul 900 ms. Volver a menos de 35 grados por 1 s: apagado suave. Caminar 10 minutos fuera de la mano y ajustar umbrales si se activa accidentalmente. Probar corte total, reposo 30 minutos para detectar autoapagado de batería, y temperatura tras 20 minutos de blanco. No provocar cortos para probar fusible.
8. Montar sin guante
Anillo en base ligera de palma con difusor opal separado 5–10 mm, bordes redondeados y cara posterior aislante. Correa por dorso sin sujetar dedos. Sensor rígido en el dorso de la mano, X hacia dedos y Z hacia fuera. ESP32, placa soldada y batería en muñequera con cierre rápido, sin comprimir piel. Si batería pesa, subirla al antebrazo. Cable silicona 22 AWG para potencia, 26 AWG para señales; conectores polarizados con bloqueo >=2 A, alivio de tracción y bucle para flexión. No usar protoboard ni Dupont en uso portátil.
- 68 mm puede exceder el ancho útil de la palma. Comprobar con plantilla antes de comprar. No usar con lluvia, conduciendo ni herramientas; retirar si se calienta. No mirar directamente los LED.
9. Recargar y resolver fallos
Apagar interruptor, desconectar batería del accesorio y recargar con cargador USB recomendado por su fabricante, fuera del cuerpo. Para programar, desconectar el mazo externo del ESP32 y alimentar solo por su USB. Si parpadea: revisar masas, tensión y DIN. Si no detecta sensor: revisar SDA/SCL y VIN 3.3 V. Si no termina calibración: dejar inmóvil. Colores incorrectos: confirmar orden GRB del anillo. Si se apaga solo en reposo: batería con modo siempre encendido; no añadir resistencias que calienten la muñeca.
10. Dimensionar batería y consumo
Presupuesto a 5 V (estimado, medir con medidor USB): placa sin radio 80–150 mA, MPU6050 hasta 10 mA, buffer y LED en negro hasta unos 20 mA adicionales. Azul pulsante con brillo máximo 96/255: unos 80–140 mA de anillo; total normal 180–300 mA, cálculo central 250 mA. Blanco limitado: anillo hasta 16×60×96/255=361 mA más reposo; reservar 550 mA totales. Firmware sin límite accidental: 960 mA de LED más placa, reservar 1.2 A. Salida 5 V 2 A, cable y conectores >=2 A, fusible 1.5 A. SS34 puede disipar hasta aproximadamente 0.6 W en fallo de brillo: aislar de piel y ventilar. No superar 96 de brillo para uso corporal.
11. Estimar autonomía realista
Capacidad nominal de batería medida a 3.7 V, no a 5 V. Energía útil estimada 0.85×3.7×mAh/1000 Wh. Horas = energía útil/(5×corriente en A). A 250 mA normal / 550 mA blanco limitado / 1.2 A sin límite: 2000 mAh = 5.0/2.3/1.0 h; 5000 mAh = 12.6/5.7/2.6 h; 10000 mAh = 25.2/11.4/5.2 h. Son estimaciones; envejecimiento y convertidor reducen resultados. Mínimo práctico 2000 mAh, recomendada 5000 mAh con salida siempre encendida; 10000 mAh suele resultar pesada en muñeca. Apagar accesorio con interruptor corta ambas líneas, pero la batería conserva consumo propio: desconectarla para almacenamiento.
12. Ajustar límites de reconocimiento
MPU6050 no detecta dedos abiertos ni dirección absoluta de la palma y no separa un gesto idéntico accidental. La activación compara la inclinación frente a reposo calibrado al encender. Filtro 21 Hz y promedio exponencial, umbrales 65/35 grados, permanencias y rechazo de aceleración reducen falsos positivos, sin garantizarlos caminando. Doble giro de muñeca cambia azul, rojo, verde, morado, blanco solo si COLOR_GESTURE=true; deshabilitado de fábrica para evitar cambios accidentales. Para máxima fiabilidad dejarlo deshabilitado y elegir colorIndex en firmware; no necesita otro sensor.
Review all connections
1. Connections between "bank" and "ESP32"
2. Connections between "fuse" and "ESP32"
3. Connections between "switch" and "ESP32"
4. Connections between "diode" and "ESP32"
5. Connections between "ring" and "ESP32"
6. Connections between "bulk" and "ESP32"
7. Connections between "buffer" and "ESP32"
8. Connections between "bypass" and "ESP32"
9. Connections between "dataR" and "ESP32"
10. Connections between "pullR" and "ESP32"
11. Connections between "imu" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_NeoPixel.h>
#include <math.h>
// Sensor en la mano: +X hacia dedos, +Z perpendicular al dorso.
constexpr int SDA_PIN=8, SCL_PIN=9, LED_PIN=14;
constexpr float ON_ANGLE=65, OFF_ANGLE=35, MAX_SPEED=0.75f;
constexpr uint32_t ON_HOLD=650, OFF_HOLD=1000, FADE_MS=900;
constexpr uint8_t MAX_LIGHT=96; // Limita brillo, incluido blanco (96/255).
constexpr bool COLOR_GESTURE=false; // Opcional: doble giro rápido sobre X.
Adafruit_MPU6050 imu;
Adafruit_NeoPixel leds(16,LED_PIN,NEO_GRB+NEO_KHZ800);
enum State {REST, LIGHT_UP, ACTIVE, COLOR, LIGHT_DOWN};
State state=REST;
float ref[3]={0,0,1}, v[3]={0,0,1}, bias[3]={0,0,0}, envelope=0;
uint32_t sampleAt=0, renderAt=0, candidateAt=0, lastGood=0, firstTwist=0, lockUntil=0;
uint8_t colorIndex=0; bool ready=false, armed=false, twistHigh=false;
const uint8_t colors[][3]={{0,45,255},{255,0,0},{0,255,0},{180,0,255},{255,255,255}};
void dark(){leds.clear(); leds.show();}
void setup(){
Serial.begin(115200); leds.begin(); dark();
Wire.begin(SDA_PIN,SCL_PIN); Wire.setClock(100000); Wire.setTimeOut(20);
ready=imu.begin(0x68,&Wire);
if(!ready){Serial.println("MPU6050 no detectado: revisar VIN, GND, SDA y SCL. LEDs bloqueados.");return;}
imu.setAccelerometerRange(MPU6050_RANGE_4_G);
imu.setGyroRange(MPU6050_RANGE_500_DEG);
imu.setFilterBandwidth(MPU6050_BAND_21_HZ);
Serial.println("Mantener mano quieta en reposo 4 segundos para calibrar.");
}
void loop(){
uint32_t now=millis(); if(!ready)return;
static uint32_t calStart=0; static int count=0; static float sums[3]={},gs[3]={};
if(now-sampleAt>=10){
sampleAt=now; sensors_event_t a,g,t;
if(!imu.getEvent(&a,&g,&t)){ if(now-lastGood>150){dark(); armed=false; state=REST; envelope=0; count=0; calStart=0;} return; }
lastGood=now;
float av[3]={a.acceleration.x,a.acceleration.y,a.acceleration.z};
float gv[3]={g.gyro.x,g.gyro.y,g.gyro.z};
float norm=sqrtf(av[0]*av[0]+av[1]*av[1]+av[2]*av[2]);
float rawSpeed=sqrtf(gv[0]*gv[0]+gv[1]*gv[1]+gv[2]*gv[2]);
if(!armed){
if(rawSpeed>0.18f || fabsf(norm-9.80665f)>0.8f){count=0;calStart=0;for(int i=0;i<3;i++){sums[i]=0;gs[i]=0;}return;}
if(!calStart)calStart=now;
for(int i=0;i<3;i++){sums[i]+=av[i];gs[i]+=gv[i];}count++;
if(now-calStart>=3500 && count>200){
float n=sqrtf(sums[0]*sums[0]+sums[1]*sums[1]+sums[2]*sums[2]);
for(int i=0;i<3;i++){ref[i]=v[i]=sums[i]/n;bias[i]=gs[i]/count;}
armed=true;Serial.println("Listo. Elevar/inclinar mano >65 grados y mantener.");
}return;
}
for(int i=0;i<3;i++)gv[i]-=bias[i];
float speed=sqrtf(gv[0]*gv[0]+gv[1]*gv[1]+gv[2]*gv[2]);
bool stable=fabsf(norm-9.80665f)<1.4f;
if(stable)for(int i=0;i<3;i++)v[i]+=0.08f*(av[i]/norm-v[i]);
float n=sqrtf(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
float dot=(v[0]*ref[0]+v[1]*ref[1]+v[2]*ref[2])/n;
float angle=acosf(constrain(dot,-1.0f,1.0f))*180.0f/PI;
bool condition=stable && speed<MAX_SPEED && ((state==REST)?angle>ON_ANGLE:angle<OFF_ANGLE);
if(state==REST || state==ACTIVE){
if(condition){if(!candidateAt)candidateAt=now;
if(now-candidateAt>=(state==REST?ON_HOLD:OFF_HOLD)){state=state==REST?LIGHT_UP:LIGHT_DOWN;candidateAt=0;}
}else candidateAt=0;
}
if(COLOR_GESTURE && state==ACTIVE && now>=lockUntil){
bool high=gv[0]>3.5f;
if(high&&!twistHigh){
if(firstTwist && now-firstTwist>180 && now-firstTwist<900){colorIndex=(colorIndex+1)%5;state=COLOR;lockUntil=now+2000;firstTwist=0;}
else firstTwist=now;
}twistHigh=high;
}
}
if(now-renderAt>=20){
float dt=(now-renderAt)/float(FADE_MS);renderAt=now;
if(state==LIGHT_UP){envelope=fminf(1,envelope+dt);if(envelope>=1)state=ACTIVE;}
if(state==LIGHT_DOWN){envelope=fmaxf(0,envelope-dt);if(envelope<=0)state=REST;}
if(state==COLOR)state=ACTIVE;
float pulse=0.8f+0.2f*sinf(now*0.004f);
uint8_t brightness=uint8_t(MAX_LIGHT*envelope*pulse);
uint32_t rgb=leds.Color(colors[colorIndex][0]*brightness/255,colors[colorIndex][1]*brightness/255,colors[colorIndex][2]*brightness/255);
static uint32_t previous=0xFFFFFFFF;
if(rgb!=previous){for(int i=0;i<16;i++)leds.setPixelColor(i,rgb);leds.show();previous=rgb;}
}
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




