Kamil Gallardo Toledo - FAB ACADEMY
It consists of a device that measures reaction speed and the force of punches for people interested in boxing. Using a visual cue, users can learn punch combinations while receiving feedback through a virtual interface.
My project idea came about because, during my trips to the gym, I noticed that many people hit the punching bag without proper technique, and that home punching boxes do not encourage the development of correct technique either.
So I came up with the idea of designing a punching bag that encourages proper technique while being user-friendly and fun. First, I thought about games that consist of a memory sequence where different icons light up and you have to follow the sequence.
System integration
Multi-device addressing via SDA/SCL.
Structure: Includes the design and 3D printing of the frame.
Plan
Control System: Use an ESP32 and its clock to count the time between strikes and turn led sequence.
Electronics Design/Production: Design a PCB for the microcontroller that connects the lights and sensors.
Input Devices: Add sensors so we can track the speed of the strike.
Output Devices: Control the neopixels.
Interface and Application Programming: Build a simple web or mobile interface to showcase the speed of punches and show the combos.
Computer-Aided Design: Design the 3D model of the machine and the internal structure.
Idea
The plan is to make a wall-mounted punching bag with four illuminated zones that indicate where to strike, measuring the speed of the punches and featuring preloaded combos. I made a short sketch that I intend to improve soon.
During some week tasks I tried to learn about some necesarry things for my final project. Here is the progress I have.
For more information you can access to my fourth week Week 4.
My code consists of turning on three LEDs in sequence and starting a timer each time one turns on to measure the time between the LED lighting up and the button being pressed. During the Development of my code I was assisted by ChatGPT to understand the ESP32 internal timer and to be able to register big numbers.
// ---------- Pins ----------
const int ledPins[3] = {16, 17, 18};
const int btnPins[3] = {13, 12, 14};
// ---------- Time ----------
hw_timer_t *timer = NULL;
volatile unsigned long tiempo = 0; // ms
// ---------- Control ----------
int status = 0;
bool waiting = false;
// ---------- Interruption ----------
void IRAM_ATTR onTimer() {
tiempo++; // 1 ms
}
// ---------- Setup ----------
void setup() {
Serial.begin(9600);
// LEDs
for (int i = 0; i < 3; i++) {
pinMode(ledPins[i], OUTPUT);
digitalWrite(ledPins[i], LOW);
}
// Buttons
for (int i = 0; i < 3; i++) {
pinMode(btnPins[i], INPUT_PULLUP);
}
// ---------- Timer ----------
timer = timerBegin(1000000);
timerAttachInterrupt(timer, &onTimer);
timerAlarm(timer, 1000, true, 0);
timerStart(timer);
iniciateLED();
}
// ---------- Loop ----------
void loop() {
if (waiting) {
if (digitalRead(btnPins[status]) == LOW) {
waiting = false;
Serial.print("LED ");
Serial.print(status + 1);
Serial.print(" -> Tiempo: ");
Serial.print(tiempo);
Serial.println(" ms");
delay(300);
nextLED();
}
}
}
// ---------- Functions ----------
void iniciateLED() {
turnOff();
tiempo = 0;
digitalWrite(ledPins[status], HIGH);
waiting = true;
Serial.print("LED ");
Serial.print(status + 1);
Serial.println(" encendido...");
}
void nextLED() {
status++;
// If LED number 3 already ended:
if (status >= 3) {
waiting = false; // NO MORE BUTTONS LEFT
turnOff(); // Turn off LEDs
Serial.println("---- Cicle ended ----");
return;
}
iniciateLED();
}
void turnOff() {
for (int i = 0; i < 3; i++) {
digitalWrite(ledPins[i], LOW);
}
}
For more information you can access to my ninth week Week 9.
I made this sensor with the help of Adrian Torres documentation, Neil Gershenfeld's examples and Robert Hart's page.
What this sensor does is that its reading increases when we bring the plates closer together, since the distance between the two copper pieces decreases, causing a change in capacitance. The closer the plates are, the greater the capacitance.
Step Response. This board only has the pins needed to connect the step-response sensor to the microcontroller. It consists of two analog pins; one is connected to GND and the other to 3.3V via two 1-megohm resistors. After that, I simply added pins for power.
1. Then I went to the PCB editor and with the Route single track I connected every component.
Calculator tool. Before defining the size, it is important to calculate it using the calculator tool given by KiCad. To do that we first have to go to the start menu and open the Calculator tool.
add the Current (I) and the Temperature rise we are expecting our PCB to have and look fo the result the calculator will give back to us in the right top side. The calculator works by using a formula explained at the bottom.
Track thickness. To change the track thickness we must go to the top tool section and click on Track use netclass width. Subsequently, select Edit Pre-defined Sizes.
This are the parameters for each process in Mods. If you want to learn more go to Week 8.
Parameters.• The outline width is 2 mm and its layer is Edge.Cuts.• The track’s width is 0.8 mm- 2 mm and its layer is F.Cu.• The Holes layer is User.1.
Drilling - MODS.
• Tool width. 0.8 mm• Speed. 0.5 mm/s• Origin (x,y,z). (0,0,0)• Offset number. 1
Cutting - MODS.
• Tool width. 0.39 mm• Speed. 4 mm/s• Origin (x,y,z). (0,0,0)•Offset number. 3
Outline - MODS.
• Tool width. 2 mm• Speed. 4 mm/s• Origin (x,y,z). (0,0,0)• Offset number. 1
~ Method: Step response capacitive sensing (TX: D10, RX: A2).
~ Output: Dual WS2812B visual feedback via Pin D3.
~ Logic: Signal mapping and range filtering for proximity detection.
#include <Adafruit_NeoPixel.h>
// -------- CONFIGURACIÓN --------
#define PIN_NEO 3
#define NUM_PIXELS 2
Adafruit_NeoPixel pixels(NUM_PIXELS, PIN_NEO, NEO_GRB + NEO_KHZ800);
long result;
int analog_pin = A2;
int tx_pin = D10;
void setup() {
pinMode(tx_pin, OUTPUT);
Serial.begin(115200);
pixels.begin();
pixels.clear();
pixels.show();
}
long tx_rx() {
int read_high, read_low, diff;
long sum = 0;
for (int i = 0; i < 100; i++) {
digitalWrite(tx_pin, HIGH);
read_high = analogRead(analog_pin);
delayMicroseconds(100);
digitalWrite(tx_pin, LOW);
read_low = analogRead(analog_pin);
diff = read_high - read_low;
sum += diff;
}
return sum;
}
void loop() {
result = tx_rx();
long mapped_result = map(result, 15000, 25000, 0, 1024);
if (mapped_result >= 30000 && mapped_result <= 35000) {
for(int i=0; i < NUM_PIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(255, 0, 0));
}
pixels.show();
} else {
pixels.clear();
pixels.show();
}
delay(50);
}
For more information you can access to my ninth week Week 11.
The diagram shows the wiring of the system powered by the 5V output of the XIAO ESP32-C6. This voltage rail supplies power to the NeoPixels, while all GND connections are tied directly to the XIAO’s GND, creating a common ground that ensures stable operation and proper signal reference across the system.
Additionally, the system includes five push buttons used for input commands. These buttons are connected directly to the XIAO pins from D0 to D4 and use the internal pull-up resistor configuration, meaning each pin reads HIGH by default and switches to LOW when the button is pressed.
From pin D5, a 220 Ω resistor is placed in series with the data line that connects to the input (DIN) of the NeoPixels. This resistor helps protect the data line from voltage spikes and improves signal integrity. The NeoPixels are connected in series, where the data flows from the first LED to the next (DOUT to DIN), allowing the microcontroller to control all LEDs through a single data pin.
~ Network: WiFi connection to MQTT Broker (EMQX).
~ System: MQTT publish "Macarena" in the topic xiao/boton
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
// -------- WIFI --------
const char* ssid = "iPhone de Derek";
const char* password = "password";
// -------- MQTT --------
const char* mqttServer = "broker.emqx.io";
const int mqttPort = 1883;
// -------- PINES --------
#define PIN_BOTON D0
#define PIN_KAM D1
#define PIN_RGB D5
#define NUM_LEDS 10
#define LED 23
// -------- OBJETOS --------
WiFiClient esp32Client;
PubSubClient mqttClient(esp32Client);
Adafruit_NeoPixel pixels(NUM_LEDS, PIN_RGB, NEO_GRB + NEO_KHZ800);
// -------- VARIABLES --------
int var = 0;
String resultS = "";
uint32_t pixelHue = 0;
bool lastState = HIGH;
bool before = HIGH;
// -------- WIFI --------
void wifiInit() {
Serial.print("Conectándose a ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println("\nConectado a WiFi");
Serial.println(WiFi.localIP());
}
// -------- CALLBACK MQTT --------
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Mensaje recibido [");
Serial.print(topic);
Serial.print("] ");
char payload_string[length + 1];
memcpy(payload_string, payload, length);
payload_string[length] = '\0';
int resultI = atoi(payload_string);
var = resultI;
resultS = "";
for (int i = 0; i < length; i++) {
resultS += (char)payload[i];
}
Serial.println(resultS);
}
// -------- RECONEXIÓN MQTT --------
void reconnect() {
while (!mqttClient.connected()) {
Serial.print("Intentando MQTT...");
String clientId = "Kamilovich-" + String(random(0xffff), HEX);
if (mqttClient.connect(clientId.c_str())) {
Serial.println("Conectado");
mqttClient.subscribe("fab_test_mine");
} else {
Serial.println(" fallo, reintentando...");
delay(3000);
}
}
}
// -------- SETUP --------
void setup() {
Serial.begin(115200);
pinMode(PIN_BOTON, INPUT_PULLUP);
pinMode(PIN_KAM, INPUT_PULLUP);
pinMode(LED, OUTPUT);
pixels.begin();
pixels.setBrightness(50);
pixels.show();
wifiInit();
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCallback(callback);
}
// -------- LOOP --------
void loop() {
if (!mqttClient.connected()) {
reconnect();
}
mqttClient.loop();
apagarkamil();
leerBoton();
if (var == 0) {
efectoGamer();
}
else if (var == 1) {
efectoRespiracion();
}
}
// -------- BOTÓN --------
void leerBoton() {
bool estado = digitalRead(PIN_BOTON);
if (estado == LOW && lastState == HIGH) {
mqttClient.publish("xiao/boton", "Macarena");
Serial.println("Enviado: Macarena");
delay(50);
}
lastState = estado;
}
// -------- BOTÓN 2 --------
void apagarkamil() {
bool como = digitalRead(PIN_KAM);
if (como == LOW && before == HIGH) {
mqttClient.publish("xiao/boton", "APAGAOS EN NOMBRE DE LO BUENO Y DE LO HONESTO");
Serial.println("Enviado: APAGAOS EN NOMBRE DE LO BUENO Y DE LO HONESTO");
delay(50);
}
before = como;
}
// -------- EFECTO GAMER --------
void efectoGamer() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 15) return;
for (int i = 0; i < pixels.numPixels(); i++) {
int hueOffset = i * (65536 / pixels.numPixels());
pixels.setPixelColor(i, pixels.gamma32(
pixels.ColorHSV(pixelHue + hueOffset)
));
}
pixels.show();
pixelHue += 256;
lastUpdate = millis();
}
// -------- EFECTO RESPIRACIÓN --------
void efectoRespiracion() {
static int brillo = 0;
static int direccion = 5;
brillo += direccion;
if (brillo <= 0 || brillo >= 255) {
direccion *= -1;
}
for (int i = 0; i < pixels.numPixels(); i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, brillo));
}
pixels.show();
delay(20);
}
This diagram represents the wiring of the system powered by a Seeed Studio XIAO ESP32-C6. The red 3.3V line and black GND line form a common power rail that distributes energy to two WS2812B NeoPixels, an OLED display, and a ESP32-WROOM-32 dev module. The green signal line originates from pin D3, passing through a 220Ω resistor (used to protect the data pin from voltage spikes) before reaching the DIN port of the first NeoPixel; the signal then chains from DOUT to the next pixel's DIN. This setup allows the XIAO to act as the controller of the ESP32 and the Neopixels, managing both local visual feedback.
~ Network: WiFi connection to MQTT Broker (EMQX).
~ System: MQTT Callback for "Macarena" mode and Gamer effects.
~ Setup: PIN_BOTON (D0), PIN_RGB (D3), NUM_LEDS (2).
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_NeoPixel.h>
// -------- WIFI --------
const char* ssid = "iPhone de Derek";
const char* password = "9414902012";
// -------- MQTT --------
const char* mqttServer = "broker.emqx.io";
const int mqttPort = 1883;
// -------- PINES --------
#define PIN_BOTON D0
#define PIN_RGB D3
#define NUM_LEDS 2
#define LED 23
// -------- OBJETOS --------
WiFiClient esp32Client;
PubSubClient mqttClient(esp32Client);
Adafruit_NeoPixel pixels(NUM_LEDS, PIN_RGB, NEO_GRB + NEO_KHZ800);
// -------- VARIABLES --------
int var = 0;
String resultS = "";
uint32_t pixelHue = 0;
bool lastState = HIGH;
bool modoMacarena = false;
void wifiInit() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void callback(char* topic, byte* payload, unsigned int length) {
resultS = "";
for (int i = 0; i < length; i++) {
resultS += (char)payload[i];
}
if (String(topic) == "xiao/boton") {
if (resultS == "Macarena") {
modoMacarena = true;
for (int i = 0; i < NUM_LEDS; i++) {
pixels.setPixelColor(i, pixels.Color(255, 0, 0));
}
pixels.show();
} else {
modoMacarena = false;
}
}
var = atoi(resultS.c_str());
}
void reconnect() {
while (!mqttClient.connected()) {
String clientId = "Kamilovich-" + String(random(0xffff), HEX);
if (mqttClient.connect(clientId.c_str())) {
mqttClient.subscribe("xiao/boton");
} else {
delay(3000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_BOTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
pixels.begin();
pixels.setBrightness(50);
wifiInit();
mqttClient.setServer(mqttServer, mqttPort);
mqttClient.setCallback(callback);
}
void loop() {
if (!mqttClient.connected()) { reconnect(); }
mqttClient.loop();
if (!modoMacarena) { efectoGamer(); }
leerBoton();
if (var == 0) { digitalWrite(LED, LOW); }
else if (var == 1) { digitalWrite(LED, HIGH); }
}
void leerBoton() {
bool estado = digitalRead(PIN_BOTON);
if (estado == LOW && lastState == HIGH) {
mqttClient.publish("xiao/boton", "Macarena");
delay(200);
}
lastState = estado;
}
void efectoGamer() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 15) return;
for (int i = 0; i < pixels.numPixels(); i++) {
int hueOffset = i * (65536 / pixels.numPixels());
pixels.setPixelColor(i, pixels.gamma32(pixels.ColorHSV(pixelHue + hueOffset)));
}
pixels.show();
pixelHue += 256;
lastUpdate = millis();
}
This diagram shows the I2C communication setup between an ESP32-WROOM-32 development board and an OLED display. The blue line connects the SDA (Serial Data) pin of the OLED to GPIO 21 on the ESP32, while the yellow line connects the SCL (Serial Clock) pin to GPIO 22. These two pins are the standard hardware I2C default for the ESP32, allowing the microcontroller to send graphical data and text to the screen using a synchronous serial protocol.
~ Device: ESP32-WROOM-32 Receiver.
~ Display: SSD1306 OLED via I2C (SDA: 21, SCL: 22).
~ Action: Real-time visualization of messages from topic xiao/boton.
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// -------- CONFIGURACIÓN OLED --------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// -------- WIFI & MQTT --------
const char* ssid = "iPhone de Derek";
const char* mqttServer = "broker.emqx.io";
WiFiClient esp32Client;
PubSubClient mqttClient(esp32Client);
void actualizarOLED(String mensaje) {
display.clearDisplay();
display.setCursor(0, 0);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.println(mensaje);
display.display();
}
void callback(char* topic, byte* payload, unsigned int length) {
String mensaje = "";
for (int i = 0; i < length; i++) { mensaje += (char)payload[i]; }
actualizarOLED(mensaje);
}
void reconnect() {
while (!mqttClient.connected()) {
String clientId = "Kamo_Display_" + String(random(0xffff), HEX);
if (mqttClient.connect(clientId.c_str())) {
mqttClient.subscribe("xiao/boton");
} else { delay(3000); }
}
}
void setup() {
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { for(;;); }
display.clearDisplay();
actualizarOLED("Esperando WiFi...");
wifiInit();
mqttClient.setServer(mqttServer, 1883);
mqttClient.setCallback(callback);
}
void loop() {
if (!mqttClient.connected()) { reconnect(); }
mqttClient.loop();
}
| QTY | COMPONENT | DESCRIPTION | STATUS |
|---|---|---|---|
| 1 | ESP32 WROOM 32 | Primary Processing Core & Signal Management. | ACTIVE |
| 1 | USB TYPE A PORT | Main Power Input Interface. | ACTIVE |
| 1 | AMS1117 | Voltage Regulator (Stable 3.3V). | ACTIVE |
| 4 | 10uF Capacitors | Low-frequency power line filtering. | ACTIVE |
| 4 | 100nF Capacitors | High-frequency noise decoupling. | ACTIVE |
| 1 | 10K Resistor | Logic stability (Pull-up/down). | ACTIVE |
| 3 | 1M Resistor | High-impedance piezo signal biasing. | ACTIVE |
| 1 | 5V-3A Charger | Power source. | ACTIVE |
| 1 | FTDI FT232RL | USB-Serial for coding the ESP32. | ACTIVE |
| 1 | 5m NeoPixel Wire | High-intensity visual feedback array. | ACTIVE |
| 4 | Piezoelectric Sensors | Impact detection & reaction timing. | ACTIVE |