BeerPong Bot
Project plan
The goal of the project is to build a robot which can aim and throw ping pong balls into cups with a reasonable degree of accuracy. The launcher will have a twin-roller design, where the ping pong ball is pushed in between two spinning rollers, launching the ball into the air. The range of the throw is adjusted by changing the speed of the rollers and the sideways motion is achieved with a linear rail system and a stepper motor.
Pre-circus update
The first prototype frame of the bot has been built. Sideways linear movements are functional, and the launcher platform is built. The launcher however does not have the desired functionality, as the 12V DC motor is not powerful enough to achieve the necessary launch speeds. Overloading the motor with 24V achieves the desired speeds, but the vibrations from the needlessly complicated gear assembly are not optimal. As a result, a different style of launcher assembly will be attempted, probably with two encoder motors instead of a single, non-encoded motor. This will remove the need for the mechanical linkages, reducing friction while still allowing for relatively accurate speed control of the rollers. Once a new launcher model is designed and built, all that remains is to implement the remaining hardware as well as finalizing the circuitry, programming, as well as doing some testing to catch any unexpected flaws in the design.
Some pictures of the pre-circus build
Final version
Below is a video of the final working model from the Mechatronic Circus. Based on very limited testing, the success rate of the launcher seemed to be between 50 and 75 percent. I was able to get one sequence of six consecutive hits (ie. a flawless game) during the circus. The robot can't actually sense the distance of the cups nor the position, as it simply trusts that the cups are centered close to the centerline of the robot at a range of approximately 155 cm. I tried to implement an ultrasound range sensor, but was unable to get reliable readings. Upon startup the robot zeroes itself to the limit switch placed at the end of the linear rail, and centers its position. From here, it listens to the serial connection to a laptop running the Arduino IDE software. The target cups are labelled with numbers from 1-6, and once a corresponding number command is sent to the robot it moves to the correct position and ramps up the speed of the motors to achieve the correct launch speed for the given cup. Once the robot is ready to launch, an RC servo pushes the ping pong ball in between the spinning rollers, sending the ball toward the cups.
Mechanical parts:
The required 3d-printed parts for the project are pictured below, .stl and .step files will also be provided at the bottom of the page. All parts were printed using an Anycubic Kobra Max with Prusament PLA.
For the tightener nut, press an M8 threaded insert into the base of the part. This threads onto an M8 bolt which in turn is tightened around base of the fork piece with a regular M8 hex nut.
In addition to the 3d-printed parts, we have some standard components. The two linear rails are standard SBR16 rails with a length of 600 mm, mounted on a flat piece of 22 mm plywood. The gap between the axis of the rails is 70 mm. Driving the launcher's sideways motion is a NEMA17 stepper motor linked to the launcher via GT2 pulley and belt. To attach the DC motors to the launcher, I cut some sheetmetal pieces from 1mm stainless steel with a waterjet cutter. To couple the DC motors to the rollers I machined two 8mm diameter steel shafts with a 2mm hole and M3 set screw at one end and an M8 thread at the other end. Drawings for these provided at the end of the page. I also machined the "rims" of the rollers from POM plastic to attach them to foam wheels, which I looted from old RC-car remotes. I would recommend using standard foam wheels instead, I just had these ones laying around as well as access to a lathe and a mill, so I went with what was easiest for me. The diameter of the wheels I used was approximately 59 millimeters. To stabilize the rotating shafts I used some flanged ball bearings I found from a drawer that happened to have 8 mm bore width. You can freely adjust the 3d-print models to accommodate different sized standard parts with the .step files provided at the bottom of the page.
Sheetmetal part for mounting the DC motors
The steel shaft for coupling the motors with the wheels
Electronics:
The microcontroller used was the Arduino UNO R3.
List of the basic components:
Component | count | connections |
L298N motor driver | 1 | ENA1 and ENA2 to any PWM pins, IN1 and IN4 to 5V, IN2 and IN3 to ground, outputs to corresponding motors (swap motor connections to reverse rotation). |
DC motor | 2 | Motor positive to L298N 12V output, motor negative and encoder negative to ground. One of the encoder outputs to either digital pin 2 or 3. Encoder positive to 5V. |
RC-servo of your choosing | 1 | Signal to any PWM pin, positive to 5V and negative to ground. |
Stepper driver (I used A4988) | 1 | STEP and DIR to any digital I/O pins, VMOT to 12V, VDD to 5V, GND to ground, stepper coil connections to stepper motor, reset and sleep connected to each other. |
NEMA17 stepper motor | 1 | Connected to stepper driver. Coil wiring depends on your motor. |
limit switch of your choosing | 1 | Common pin to ground, N-O to any digital I/O pin. |
1000uF capacitor | 1 | Connected parallel to 5V circuit. |
100uF capacitor | 1 | Connected parallel to stepper driver 12V connection. |
12V 3A power supply | 1 | (NOTE:Using a more powerful power supply would midigate the need for a capacitor in the 5V circuit) |
Software:
The software is written in Arduino C, utilizing the libraries Servo.h, ArduPID.h and ezButton.h.
The source code file of the robot in .ino format:
Code looks something like this:
#include <Servo.h>
#include <ArduPID.h>
#include <ezButton.h>
// variables to store pin numbers
const int step = 8;
const int dir = 7;
const int ena1 = 5;
const int ena2 = 6;
const int enc1 = 3; // IMPORTANT: only pins 2 and 3 can be used with interrupts on Arduino UNO R3
const int enc2 = 2; // IMPORTANT: only pins 2 and 3 can be used with interrupts on Arduino UNO R3
// variables to operate stepper
int counter = 0;
const int delayStep = 800;
// variables for motor control
long previousT = 0;
long previousT2 = 0;
double dT1 = 0;
double dT2 = 0;
double setpoint = 1570;
double setpoint2 = 1570;
double pwm = 0;
double pwm2 = 0;
double p = 0.8;
double i = 0.001;
double d = 0.25;
double rpm = 0;
double rpm2 = 0;
// variables for loop() function
int previousCups = 0;
int cups = 0;
ArduPID myController1; // initialize PID control for motor 1
ArduPID myController2; // initialize PID control for motor 2
Servo myservo; // initialize servo object
ezButton limitSwitch(12); // attach limitswitch to pin 12
void setup() {
Serial.begin(9600); // begin serial connection
pinMode(step, OUTPUT); // set pinModes
pinMode(dir, OUTPUT);
pinMode(enc1,INPUT);
pinMode(enc2,INPUT);
pinMode(ena1,OUTPUT);
pinMode(ena2,OUTPUT);
myservo.attach(11); // attach servo to pin 11 (NOTE: PWM functionality for pins 9 and 10 is disabled while <servo.h> is active)
myservo.write(0); // ensure that servo is in the correct position
limitSwitch.setDebounceTime(50); // debounce the limitswitch
initializeStepper(); // find the zero position
myController1.begin(&rpm, &pwm, &setpoint, p, i, d); // begin PID control
myController2.begin(&rpm2, &pwm2, &setpoint2, p, i, d);
attachInterrupt(digitalPinToInterrupt(enc1), readEncoder, RISING); // set up interrupts for the encoder outputs (pins 2 and 3)
attachInterrupt(digitalPinToInterrupt(enc2), readEncoder2, RISING);
myController1.setOutputLimits(0, 255); // limit the PID controller outputs
myController2.setOutputLimits(0, 255);
myController1.start();
myController2.start();
}
void loop() {
if (previousCups != cups) { //check if a new cup number has been input
switch(cups) {
case 1: //cup 1
setpoint = 1565; // motor speed setpoints to first row
setpoint2 = 1565;
Stepleft(counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
case 2: //cup 2
setpoint = 1610; // motor speed setpoints to second row
setpoint2 = 1610;
Stepleft(245 + counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
case 3: //cup 3
setpoint = 1610; // motor speed setpoints to second row
setpoint2 = 1610;
Stepright(245 - counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
case 4: //cup 4
setpoint = 1650; // motor speed setpoints to third row
setpoint2 = 1650;
Stepright(490 - counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
case 5: //cup 5
setpoint = 1650; // motor speed setpoints to third row
setpoint2 = 1650;
Stepleft(counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
case 6: //cup 6
setpoint = 1650; // motor speed setpoints to third row
setpoint2 = 1650;
Stepleft(490 + counter); // move to position
stabilize(); // stabilize the speed
myservo.write(45); // launch the ball
delay(200); // wait for servo
myservo.write(0); // return servo
delay(200);
break;
}
previousCups = cups;
cups = 0;
}
if(Serial.available()){ // check for new input from serial
String a = Serial.readStringUntil('\n'); // read new input from serial
a.trim();
cups = a.toInt(); // store value
}
}
void stabilize() { // function to stabilize motor speed
int time = 0;
int pwmtotal = 0;
int pwmtotal2 = 0;
float total = 0.0;
while (setpoint - rpm > 10) { // first wait for motor to reach the setpoint
runPID();
}
int prevtime = millis();
while (time < 3000) { // calculate the average speed over 3 seconds
runPID();
pwmtotal += pwm;
pwmtotal2 += pwm2;
total = total + 1.0;
time += millis()-prevtime;
prevtime = millis();
}
float avg1 = (float)pwmtotal / total;
float avg2 = (float)pwmtotal2 / total;
float avg = (avg1 + avg2) / 2;
analogWrite(ena1, (int)avg); // set the motors at the calculated pwm value
analogWrite(ena2, (int)avg);
Serial.print(rpm); // print the launch values to serial
Serial.print(", ");
Serial.println(rpm2);
}
void runPID() { // function to control motor speed
myController1.compute(); // compute pid values
myController2.compute();
if (rpm > 1000) { // check if the current speed is high enough
analogWrite(ena1, (int)pwm);
analogWrite(ena2, (int)pwm2);
} else { // ramp up the speed to avoid drawing too much current
analogWrite(ena1, 150);
analogWrite(ena2, 150);
}
Serial.print(rpm); // print rpm and pwm values to serial
Serial.print(", ");
Serial.println(pwm);
}
void initializeStepper() { // function to find zero point of sideways axis
digitalWrite(dir, HIGH); // set direction to left
limitSwitch.loop(); // start listening to the limitswitch
int done = 0;
while(!done) { // continuosly poll the limitswitch to find out if contact has been made
limitSwitch.loop();
if(limitSwitch.isPressed() || limitSwitch.getState() == LOW) { // check the state of the limitswitch
done = 1;
} else { // if limitswitch is not pressed, continue moving left
digitalWrite(step, HIGH);
delayMicroseconds(delayStep);
digitalWrite(step, LOW);
delayMicroseconds(delayStep);
}
}
Stepright(1280); // center the launcher
counter = 0; // reset step counter to middle position
}
void Stepleft(int count) { // function to move the launcher to the left
if(counter > -1280) { // check if left edge has been reached
if (count < 0) { // check if input from loop() is negative
Stepright(-count); // move right instead if count is negative
} else {
digitalWrite(dir, HIGH); // set direction to left
for (int i = 0; i < count; i++) { // move launcher <count> steps
digitalWrite(step, HIGH);
delayMicroseconds(delayStep);
digitalWrite(step, LOW);
delayMicroseconds(delayStep);
counter--; // decrement counter variable
}
}
} else {
Serial.println("Left edge reached!!!"); // inform user that bad things are happening
}
}
void Stepright(int count) { // function to move the launcher to the right
if (counter < 1280) { // check if right edge has been reached
if (count < 0) { // check if input from loop() is negative
Stepleft(-count); // move left instead if count is negative
} else {
digitalWrite(dir, LOW); // set direction to right
for (int i = 0; i < count; i++) { // move launcher <count> steps
digitalWrite(step, HIGH);
delayMicroseconds(delayStep);
digitalWrite(step, LOW);
delayMicroseconds(delayStep);
counter++; // increment counter variable
}
}
} else {
Serial.println("Right edge reached!!!"); // inform user that bad things are happening
}
}
void readEncoder(){ // read values from motor encoder
dT1 = micros() - previousT; // calculate the time between pulses
previousT = micros();
rpm = 60/(11*(dT1/1000000)); // calculate rpm
}
void readEncoder2(){ // read values from motor encoder
dT2 = micros() - previousT2; // calculate the time between pulses
previousT2 = micros();
rpm2 = 60/(11*(dT2/1000000)); // calculate rpm
}