Robotics Project – Gesture Drum Bot

Music has always been an important part of my life, and it’s something I listen to everyday, whether I need to relax or find motivation. The rhythm and energy of beats have a way of bringing focus and emotions together, and inspired me to incorporate a music element into this project. For this project, I wanted to design a simple robot that could detect hand movements, respond dynamically, and perform an expressive action by drumming to the rhythm of my hand movements.

Project Overview

The Gesture Drum bot uses the user’s hand as a virtual conductor. And when their hand moves toward or away from the ultrasonic sensor in rhythm, the Arduino measures these distance changes and creates a beat.

Each time a beat is detected:

  • The servo taps like a drumstick
  • The buzzer plays a short musical note with a random
  • The LCD screen briefly flashes “BEAT!” and then updates the BPM

Tinkercad Design

Image of Circuit Design in Tinkercad

I started by designing and simulating a prototype in Tinkercad. This process helped me to first verify the logic of the code and wiring for different components.

The wires in the circuit design are also color coded to make it more accessible:

Green – PINs

Yellow – Ground

Red – Power (5V)

Blue – SDA / SCL

Circuit Wiring

ComponentPIN connectionFunction
Ultrasonic SensorVCC -> 5V,
GND -> GND,
SIG -> D9
Measures hand distance
Servo MotorSIG -> D6,
VCC -> 5V,
GND -> GND
Moves motor based on the distance
Piezo Buzzer(+) -> D3,
(-) -> GND
Plays random tone when beat triggered
LCDSDA -> A4,
SCL -> A5,
VCC -> 5V,
GND -> GND
Displace distance and BPM.

Above is the schematic diagram of the circuit design. Where it illustrates how all the components are connected to each other and the Arduino board.

Bill of Materials (BOM)

ComponentQuantityDescription
Arduino UNO1Main board
Ultrasonic Distance Sensor1Detects distance of hand movement
Servo Motor1acts as drumstick arm
Piezo Buzzer1plays a tone for each beat
PCF8574-based, 32 (0x20) LCD 16 x 2 (I2C)1displays data
LCBG LED RGB1LED that can display any color
1 kΩ Resistor3resistor for LED
BreadBoard + Jumper Wiresfor connections

Code Logic

Entire Source Code
#include <Servo.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x20, 16, 2);
Servo arm;

//pins
const int ultrasonic_pin = 9;
const int servo_pin = 6;
const int buzzer_pin = 3;

//window
const int min_dist = 60;
const int max_dist = 150;

//beat
float lastDistance = 0;
float lastVelocity = 0;
bool approaching = false;

unsigned long lastBeat = 0;
const unsigned long min_beat_gap = 250;

//BPM
unsigned long history[4] = {0};
int Pos = 0;
bool Full = false;
float bpm = 0;

//Notes
const int notes[] = {196,220,247,294,330,392,440,494};
const int note_count = sizeof(notes) / sizeof(int);

void setup() {
  Serial.begin(115200);
  
  pinMode(buzzer_pin, OUTPUT);
  digitalWrite(buzzer_pin, LOW);
  
  //setup lcd
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  //reset servo position
  arm.attach(servo_pin);
  arm.write(90);
  
  randomSeed(analogRead(A0));
  //start timer
  lastBeat = millis();

}

void loop(){
  unsigned long now = millis();
  
  //Read distance
  float dist = readDistanceCM();
  //positive value, continues
  bool valid = (dist>0);
  
  if (valid){
    //servo follows hand
    arm.write(distanceToAngle(dist));
  }
  
  //Detect beat
  float velocity = lastDistance - dist;
  //within the window that we defined
  bool inRange = (dist >= min_dist && dist <= max_dist);

  //valid hand movement, continues
  if (inRange && velocity > 0.4) {
    approaching = true;
  }
  
  //for detecting hand movement coming back
  bool reversed = (lastVelocity > 0 && velocity < 0);
  
  if (valid && approaching && reversed && (now - lastBeat > min_beat_gap)){
    triggerBeat();
    record(now);
    approaching = false;
  }
  
  lastVelocity = velocity;
  lastDistance = dist;
  
  //lcd
  static unsigned long lastLCD = 0;
  if (now - lastLCD > 200) {
    lastLCD = now;
    updateLCD(valid ? dist : -1, bpm);
  }
    
  delay(15);

}


float readDistanceCM() {
  pinMode(ultrasonic_pin, OUTPUT);
  digitalWrite(ultrasonic_pin, LOW);
  delayMicroseconds(2);
  digitalWrite(ultrasonic_pin, HIGH);
  delayMicroseconds(10);
  digitalWrite(ultrasonic_pin, LOW);
  
  //echo
  pinMode(ultrasonic_pin, INPUT);
  unsigned long dur = pulseIn(ultrasonic_pin, HIGH, 15000);
  
  if (dur==0) return -1;
  
  float cm = dur / 58.0;
  if (cm < 2 || cm > 400) return -1;
  
  return cm;
}


int distanceToAngle(float cm) {
  //detect distance inside the window
  if (cm < min_dist) cm = min_dist;
  if (cm > max_dist) cm = max_dist;
  
  //convert distance to servo angle
  float t = (cm - min_dist) / (max_dist - min_dist);
  return 35 + (int)(t*(120-35));
}

void triggerBeat() {
  int note = notes[random(note_count)];
  tone(buzzer_pin, note, 120);
  lastBeat = millis();
}

void record(unsigned long now) {
  unsigned long x = now - lastBeat;
  history[Pos] = x;
  Pos = (Pos + 1) % 4;
  
  //true once we have stored 4 intervals
  if (Pos == 0) Full = true;
  
  if (Full) {
    unsigned long sum = 0;
    for (int i=0; i<4; i++) {
      sum += history[i];
    }
    
    float avg = sum / 4.0;
    bpm = 60000.0 / avg;
    
  }
}

void updateLCD(float dist, float bpm) {
  //print distance
  lcd.setCursor(0, 0);
  if (dist < 0) {
    lcd.print("Dist: --       ");
  } else {
    lcd.print("Dist: ");
    lcd.print((int)dist);
    lcd.print("cm    ");
  }

  lcd.setCursor(0, 1);
  lcd.print("BPM: ");
  if (bpm > 0) lcd.print((int)bpm);
  else lcd.print("--");
  lcd.print("       ");
}
  1. Read distance: The ultrasonic sensor measures hand distance.
  2. Filter Noise: A smoothing algorithm for the ultrasonic sensor to remove inconsistent values that might affect consistency
  3. Detect Beat: If the hand moves toward the sensor and then reverses direction quickly, then it’s considered to a beat.
  4. Play Beat: After a beat is detected, the system will react to it by playing a random musical note using tone(), and move the servo slightly as a drum tap.
  5. Calculate BPM: The program stores the last few beat intervals and averages them.
  6. Display Data: The LCD continuously updates to show distance and BPM.

Code Explanation

void setup() {
  Serial.begin(115200);
  
  pinMode(buzzer_pin, OUTPUT);
  digitalWrite(buzzer_pin, LOW);
  
  //setup lcd
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  //reset servo position
  arm.attach(servo_pin);
  arm.write(90);
  
  randomSeed(analogRead(A0));
  //start timer
  lastBeat = millis();

}

The program starts with the setup() function, which runs once when the program is uploaded to Arduino. It includes all of the libraries needed and initializes all the variables.

Serial.begin(115200);

This line starts a serial port at 115200, which acts as the console and allows Arduino to send debugging messages. In addition, pinMode() sets up the buzzer’s pin so it can send sound signals. The parameters of it are the pin number and the mode (output / input).

diginalWrite(<pin>, LOW) turns off the buzzer at the start so it doesn’t make any sound when the program starts. With digitalWrite(), you are able to either set it as High (5V) or Low (0V).

Afterwards, the LCD display get initialized. And the servo is configured at a specific pin.

The code now proceeds to the loop() function which runs over and over again.

unsigned long nowMs = millis();

millis() is a built in function that returns the number of milliseconds since the program started running. And in this case, it is assigned and stored in a variable called nowMs, and is used throughout the loop for timing things like detecting the beat and stuff.

  //Read distance
  float dist = readDistanceCM();
  //positive value, continues
  bool valid = (dist>0);

//read distance function
float readDistanceCM() {
  pinMode(ultrasonic_pin, OUTPUT);
  digitalWrite(ultrasonic_pin, LOW);
  delayMicroseconds(2);
  digitalWrite(ultrasonic_pin, HIGH);
  delayMicroseconds(10);
  digitalWrite(ultrasonic_pin, LOW);
  
  //echo
  pinMode(ultrasonic_pin, INPUT);
  unsigned long dur = pulseIn(ultrasonic_pin, HIGH, 15000);
  
  if (dur==0) return -1;
  
  float cm = dur / 58.0;
  if (cm < 2 || cm > 400) return -1;
  
  return cm;
}

This section is in charge of reading the distance from the ultrasonic sensor and smoothening the data received. This is achieved by first creating a floating variable and calling the readDistanceCM() function to store the distance returned inside it. Afterwards, a boolean variable is created to check if the reading make sense. And if valid, the distance is passed on to convert it to an angle.

  //Servo follows
  if (valid) {
    int targetAngle = distanceToAngle(emaDist);
    arm.write(targetAngle);
  }

//function to convert distance to angle
int distanceToAngle(float cm) {
  //detect distance inside the window
  if (cm < min_dist) cm = min_dist;
  if (cm > max_dist) cm = max_dist;
  
  //convert distance to servo angle
  float t = (cm - min_dist) / (max_dist - min_dist);
  return 35 + (int)(t*(120-35));
}

If the distance is valid, it converts it to an angle that can be passed on to the servo using the distanceToAngle function.

 //Detect beat
  float velocity = lastDistance - dist;
  //within the window that we defined
  bool inRange = (dist >= min_dist && dist <= max_dist);

  //valid hand movement, continues
  if (inRange && velocity > 0.4) {
    approaching = true;
  }

This section is responsible for detecting whether the hand is moving toward the sensor fast enough to count as a beat gesture.

dist is the distance from the ultrasonic sensor. lastDistance is the previous loop’s distance, and is stored to compare how much the distance changed since then. In other words, if the hand moves quickly, the difference between those two values becomes bigger.

  velocity = lastDistance - dist;

Therefore, the formula above gives an estimate of how fast the hand is moving. If the hand moves closer to the sensor, then emaDist (current distance) becomes smaller, lastDistance – dist becomes positive, and this represents positive velocity, vice versa.

//within the window that we defined
  bool inRange = (dist >= min_dist && dist <= max_dist);

  //valid hand movement, continues
  if (inRange && velocity > 0.4) {
    approaching = true;
  }

The window min and max defines the detection zone that we actually consider about, if the hand isn’t in this zone, we ignore the movement, and this is used to prevent false triggers. And approaching is simply a boolean variable that is true when the hand is inside this distance zone.

if (valid && approaching && reversed && (now - lastBeat > min_beat_gap)){
    triggerBeat();
    record(now);
    approaching = false;
  }

Finally, the if statement considers whether the hand is inside the valid distance zone and is moving toward the sensor fast enough, if so, it triggers a beat by calling that function.

float readDistanceCM() {
  pinMode(ultrasonic_pin, OUTPUT);
  digitalWrite(ultrasonic_pin, LOW);
  delayMicroseconds(2);
  digitalWrite(ultrasonic_pin, HIGH);
  delayMicroseconds(10);
  digitalWrite(ultrasonic_pin, LOW);
  
  //echo
  pinMode(ultrasonic_pin, INPUT);
  unsigned long dur = pulseIn(ultrasonic_pin, HIGH, 15000);
  
  if (dur==0) return -1;
  
  float cm = dur / 58.0;
  if (cm < 2 || cm > 400) return -1;
  
  return cm;
}

Next up, this function is in charge of calculating the distance from the hand. The logic of it is to first send a short ultrasonic pulse, and wait for the echo to return. And by using this data, it is able to convert that time into a distance measurement.

  float dist = readDistanceCM();

We have actually mentioned it previously, and called it to store it inside the dist variable.

pinMode(ULTRA_PIN, OUTPUT);
digitalWrite(ULTRA_PIN, LOW);
delayMicroseconds(2);
digitalWrite(ULTRA_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(ULTRA_PIN, LOW);

This line first allows assigns a pin and mode for the sensor. And create a pulse starting with low for 2 microseconds, high for 10 microseconds, and back to low again.

pinMode(ULTRA_PIN, INPUT);

After that, it immediately switched to input mode to listen for the echo returning.

unsigned long dur = pulseIn(ULTRA_PIN, HIGH, 15000UL);
float cm = dur / 58.0;

Finally, this standard formula converts the echo received into distance in centimeters.

Above, I have explained all the essential parts of the code. Most of the program’s logic is organized inside the main loop, but to keep the code cleaner and easier, I moved any repeated or commonly used operations into separate functions.

Physical Prototype:

While I really want to build the project out in real life on a real Arduino board, my computer unfortunately couldn’t detect the port connected to it. I plan on fixing this issue, and once it’s revolved, I’ll update this post with a demonstration of the physical prototype.

For now, the Tinkercad prototype functions perfectly, and as a reader, you are able to replicate the project with the wiring and code provided.

Benefits of making a physical prototype:

Right now, when running the code online in Tinkercad, I’ve noticed some lag and unresponsiveness. And by compiling the code on a real Arduino board, this issue can be eliminated. Moreover, building a physical prototype provides a more engaging and realistic experience, as you are able to interact with it directly and can have a better user experience.

Reflections on AI

I used GPT-5 as my AI assistant throughout this project. I chose to use GPT not only because I am more familiar with it, but also because it knows me better in terms of my working style and preferences. Over time, it has learned about my personality and requirements stored as a memory, allowing it to provide to responses that I feel more suitable for me.

And in the project, I used AI for a lot of several tasks. This includes: debugging, suggesting better algorithms, and polishing my wording of this blog post to make it sound a bit smoother. In addition, since it had been a while since I last programmed with arduino, I have asked it for guidance on parts of the code.

I chose to use AI because it helps me to save time, while still allowing active learning for myself through problem solving, thinking creatively, and deepen my understanding of coding.

Source Code
#include <Servo.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x20, 16, 2);
Servo arm;

//pins
const int ultrasonic_pin = 9;
const int servo_pin = 6;
const int buzzer_pin = 3;

//window
const int min_dist = 60;
const int max_dist = 150;

//beat
float lastDistance = 0;
float lastVelocity = 0;
bool approaching = false;

unsigned long lastBeat = 0;
const unsigned long min_beat_gap = 250;

//BPM
unsigned long history[4] = {0};
int Pos = 0;
bool Full = false;
float bpm = 0;

//Notes
const int notes[] = {196,220,247,294,330,392,440,494};
const int note_count = sizeof(notes) / sizeof(int);

void setup() {
  Serial.begin(115200);
  
  pinMode(buzzer_pin, OUTPUT);
  digitalWrite(buzzer_pin, LOW);
  
  //setup lcd
  lcd.init();
  lcd.backlight();
  lcd.clear();
  
  //reset servo position
  arm.attach(servo_pin);
  arm.write(90);
  
  randomSeed(analogRead(A0));
  //start timer
  lastBeat = millis();

}

void loop(){
  unsigned long now = millis();
  
  //Read distance
  float dist = readDistanceCM();
  //positive value, continues
  bool valid = (dist>0);
  
  if (valid){
    //servo follows hand
    arm.write(distanceToAngle(dist));
  }
  
  //Detect beat
  float velocity = lastDistance - dist;
  //within the window that we defined
  bool inRange = (dist >= min_dist && dist <= max_dist);

  //valid hand movement, continues
  if (inRange && velocity > 0.4) {
    approaching = true;
  }
  
  //for detecting hand movement coming back
  bool reversed = (lastVelocity > 0 && velocity < 0);
  
  if (valid && approaching && reversed && (now - lastBeat > min_beat_gap)){
    triggerBeat();
    record(now);
    approaching = false;
  }
  
  lastVelocity = velocity;
  lastDistance = dist;
  
  //lcd
  static unsigned long lastLCD = 0;
  if (now - lastLCD > 200) {
    lastLCD = now;
    updateLCD(valid ? dist : -1, bpm);
  }
    
  delay(15);

}


float readDistanceCM() {
  pinMode(ultrasonic_pin, OUTPUT);
  digitalWrite(ultrasonic_pin, LOW);
  delayMicroseconds(2);
  digitalWrite(ultrasonic_pin, HIGH);
  delayMicroseconds(10);
  digitalWrite(ultrasonic_pin, LOW);
  
  //echo
  pinMode(ultrasonic_pin, INPUT);
  unsigned long dur = pulseIn(ultrasonic_pin, HIGH, 15000);
  
  if (dur==0) return -1;
  
  float cm = dur / 58.0;
  if (cm < 2 || cm > 400) return -1;
  
  return cm;
}


int distanceToAngle(float cm) {
  //detect distance inside the window
  if (cm < min_dist) cm = min_dist;
  if (cm > max_dist) cm = max_dist;
  
  //convert distance to servo angle
  float t = (cm - min_dist) / (max_dist - min_dist);
  return 35 + (int)(t*(120-35));
}

void triggerBeat() {
  int note = notes[random(note_count)];
  tone(buzzer_pin, note, 120);
  lastBeat = millis();
}

void record(unsigned long now) {
  unsigned long x = now - lastBeat;
  history[Pos] = x;
  Pos = (Pos + 1) % 4;
  
  //true once we have stored 4 intervals
  if (Pos == 0) Full = true;
  
  if (Full) {
    unsigned long sum = 0;
    for (int i=0; i<4; i++) {
      sum += history[i];
    }
    
    float avg = sum / 4.0;
    bpm = 60000.0 / avg;
    
  }
}

void updateLCD(float dist, float bpm) {
  //print distance
  lcd.setCursor(0, 0);
  if (dist < 0) {
    lcd.print("Dist: --       ");
  } else {
    lcd.print("Dist: ");
    lcd.print((int)dist);
    lcd.print("cm    ");
  }

  lcd.setCursor(0, 1);
  lcd.print("BPM: ");
  if (bpm > 0) lcd.print((int)bpm);
  else lcd.print("--");
  lcd.print("       ");
}

Comments

3 Responses to “Robotics Project – Gesture Drum Bot”

  1. mcrompton Avatar
    mcrompton

    OK, this is an exciting project and you’ve ticked all of the boxes, Michael. You’ve clearly learned from the assignment and created a strong blog post documenting your work. The question that I want you to think about relates to the line between AI doing the work and you do the work. I appreciate that you have consulted ChatGPT throughout the process and have provided the transcript of that discussion. I look at things like the code generation, however, and wonder if you truly understand how every line of code works. Your explanation of the code is very general and high level, and you clearly directly used much of the code that ChatGPT generated for you. You were able to debug, which helps you learn what is going on, but I wonder if this is just a start. While the discussion of the code would also apply to the circuit design and the writing of the post itself, could I ask you to go into more detail on the code specifically within the blog post? Don’t use AI to generate or edit that explanation, simply show each section of code and explain how it works. Thanks. Resubmit when you are done.

    1. michaelh Avatar
      michaelh

      Hi Mr. Crompton, thanks for your feedback. I have reflected on how I’ve been using AI for this assignment, and it made me realize that I may have been relying on it more than I should have. In future assignments, I will take into consideration of how I can ask AI for more constructive questions that are more helpful in my learning process. In response to your feedback, I have updated the blog post and added a more detailed explanation of the code. I also rewrote most of the code myself this time and simplified the program’s logic. Thanks.

      1. mcrompton Avatar
        mcrompton

        Thank you, Michael. I appreciate your thoughtful response and your more detailed explanation of the code.

Leave a Reply

Your email address will not be published. Required fields are marked *