We will achieve this by fabricating a small field that has rails on both ends that move paddles that will hit the ball. The basic workflow would be to detect where the ball is on the board and move the paddle to align itself with the ball. When the ball is close to the paddle the solenoid hits it to send across the other way.
There are multiple ways for the paddles to position themselves correctly to hit the ball.
Camera vision
The first solution that comes into mind is to use camera vision to see where the ball is on the field.
That would mean you’d have a camera above the board, analysing the position of the ball. We found this method to be a bit overkill as you’d need to have quite a powerful device to do the calculations, at least a Raspberry pi, and it wouldn’t be visually pleasing. It would be nice to have a sleek looking standalone machine.
Capacitance
We then thought of having lines of conductive material across the board, and use capacitance to detect when the ball crosses over a line. We did some tests but discovered that the metal ball we were using wasn’t detected with the Touch pins of the ESP-32. We realised that capacitance works when an object has the capacity to absorb or emit energy. A metal ball rolling around doesn’t have that capacity.
There’s a lot of not very conclusive documentation here.
Creating shorts
We then thought that if we had two thin lines of conductive material, then when the metal ball rolls over them it would create a short that would be detectable. For this an Arduino would be enough as it’s just a matter of detecting voltage. We did some tests and we could easily detect the passage of the ball, even at hight speed. Good.
Creating shorts
Simple code to detect when the ball crosses to another column. Here’s the code we used.
Basically it reads the digital reading of the pin. Thanks to a pull-down resistor it is always 0 (LOW), unless the current goes through, which means it is 1 (HIGH).
How many columns can we have?
The Arduino Shield to control the NEMA connects to all the pins of the Arduino, but not all pins are being used.
For controlling the two axises we’ll use the pins Direction Y-Axis, Direction X-Axis, Step Pulse Y-Axis & Step pulse X-Axis, in other words (or numbers), we’ll use pin 6, 5, 3 and 2.
We’ll also use Limit X-Axis and Y-Axis, so pin 9 and 10.
We won’t use a spindle, so pins 13 and 12 are free. We won’t use the Z-axis so pin 11 and 4 are also free. We won’t use Reset/Abort, Feed Hold, Cycle Start/Resume and Coolant Enable, so pin A0 to A3. There are two other pins shown as (not used/reserved), pins A4 and A5.
In summary the free pins are:
4, 7, 11, 12, 13
A0, A1, A2, A3, A4 & A5
A total of 11 pins are free. We only need one by line, we are using 4 lines for now.
Design and laser cut an acrylic sheet with the imprints of our elements (NEMA17 stepper motor and solenoid)
Design and 3D print a support for the solenoid, so that it can be fixed on our mounting plate
3D print structural elements to make the rails stand upright
Prepare and assemble the electronics (cables)
Glue stops at both ends to prevent excessive movement
Cut and position the conductive lines from a copper sheet
Rails
We used v-slot rail to build the overall structure, following a valuable example from OpenBuilds. We had to print this part this part from Thingiverse in order to make the rails stand upright.
Mounting plate
We designed and laser-cut a mounting plate made of acrylic (10 mm) with the imprints of our elements (NEMA17 stepper motor and solenoid) in order to be able to fix our elements to the structure, and thus make it move when the stepper motor is actuated.
Solenoid holder
To fix the solenoid, which will then be used to push the ball forward, we had to design an holder that we can screw onto our mounting plate.
Lines
As mentioned above, we have made copper conductor lines and connected them to different pins in the CNC shield of the Arduino Uno, in order to be able to detect when one of these lines is crossed by a conductive material (the ball).
The following code does two main things. The first is the calibration of the stepper motor, in order to know how many steps separate the beginning from the end of the rail. The second is to update the position of the motor according to any crossed lines in the field. Later, it will actuate the solenoid to push the ball forward.
Calibration
To calibrate, we run the stepper motor in one direction until it reaches an endstop, then we go in the other direction counting all the steps the motor takes until we reach the other endstop. Once we know how many steps separate the beginning from the end, we can place the motor in the middle and start the game.
void motorCalibration()
{
endstopStatus = digitalRead(endstop);
while (endstopStatus == true)
{
// to right
motorStep(false, 1, 0);
endstopStatus = digitalRead(endstop);
}
motorStep(true, 30, 50);
endstopStatus = digitalRead(endstop);
while (endstopStatus == true)
{
// to left
motorStep(true, 1, 0);
endstopStatus = digitalRead(endstop);
totalSteps++;
}
totalSteps = totalSteps + 30;
Serial.println(totalSteps);
// send motor to center
laneSteps = totalSteps / 4;
motorStep(false, totalSteps / 2, 50);
}
Positioning
To position correctly the pad according to any crossed lines we had to think about good logic. And the simplest one is always the best.
Each lane has a numerical value between 0 and 4 and each line also has a numerical value between 0 and 3.
The game is started with the motor in the centre, in lane 2. As soon as a line is crossed, we check whether the value of the line is lower or higher than the value of the lane, which indicates whether the ball is moving to the right or to the left.
Then, knowing the direction and the line crossed, we know in which lane the motor must move. But for how many steps? For the difference between its current position and its final position multiplied by the number of steps that make up a lane.
Let's say that the previous calibration gave us 1000 steps between the beginning and the end of the track. We have 5 lanes in our field, which means that each lane has a width of 200 steps. If our motor is on lane 2 and the line 3 is crossed: 3 is higher than 2, this means that we are going to the right, and the motor has to move on 1 lane (3 - 2), which corresponds to 200 steps to the right. And so on.
void updatePos(int line)
{
if (line < position)
{
diffPosition = position - line;
motorStep(true, laneSteps * diffPosition, 50);
position = line;
}
else
{
diffPosition = (line + 1) - position;
motorStep(false, laneSteps * diffPosition, 50);
position = line + 1;
}
}
Code source
Here is full code that we used to run our prototype.
// Pong Machine - One side
// 2 4 6 8
// 1 | 3 | 5 | 7 | 9
// ----------------- A
// Stepper motor pins
#define motorEnable 8
#define motorStp 3
#define motorDir 6
// Line pins
#define line0 A2
#define line1 A1
#define line2 A0
#define line3 A3
// Line status
bool line0Status = false;
bool line1Status = false;
bool line2Status = false;
bool line3Status = false;
// Endstop pin
#define endstop 10
int endstopStatus;
// Lanes
int laneNumber = 5;
int totalSteps = 0;
int laneSteps;
// Position
int position = 2;
int diffPosition;
int moveSteps;
void motorStep(bool dir, int steps, int inDelay)
{
digitalWrite(motorDir, dir);
delay(inDelay);
for (int i = 0; i < steps; i++)
{
endstopStatus = digitalRead(endstop);
digitalWrite(motorStp, HIGH);
delayMicroseconds(800);
digitalWrite(motorStp, LOW);
delayMicroseconds(800);
}
}
void motorCalibration()
{
endstopStatus = digitalRead(endstop);
while (endstopStatus == true)
{
// to right
motorStep(false, 1, 0);
endstopStatus = digitalRead(endstop);
}
motorStep(true, 30, 50);
endstopStatus = digitalRead(endstop);
while (endstopStatus == true)
{
// to left
motorStep(true, 1, 0);
endstopStatus = digitalRead(endstop);
totalSteps++;
}
totalSteps = totalSteps + 30;
Serial.println(totalSteps);
// send motor to center
laneSteps = totalSteps / 4;
motorStep(false, totalSteps / 2, 50);
}
void updatePos(int line)
{
if (line < position)
{
diffPosition = position - line;
motorStep(true, laneSteps * diffPosition, 50);
position = line;
}
else
{
diffPosition = (line + 1) - position;
motorStep(false, laneSteps * diffPosition, 50);
position = line + 1;
}
}
void setup()
{
Serial.begin(9600);
delay(1000);
pinMode(motorEnable, OUTPUT);
pinMode(motorStp, OUTPUT);
pinMode(motorDir, OUTPUT);
pinMode(endstop, INPUT_PULLUP);
pinMode(line0, INPUT);
pinMode(line1, INPUT);
pinMode(line2, INPUT);
pinMode(line3, INPUT);
digitalWrite(motorEnable, LOW);
motorCalibration();
}
void loop()
{
// Update line status
line0Status = digitalRead(line0);
line1Status = digitalRead(line1);
line2Status = digitalRead(line2);
line3Status = digitalRead(line3);
// Update position
if (line0Status)
{
updatePos(0);
}
else if (line1Status)
{
updatePos(1);
}
else if (line2Status)
{
updatePos(2);
}
else if (line3Status)
{
updatePos(3);
}
}
Here is the current version of our working prototype.
With a ball
the metalic ball, as previously mentionned has not enough contact surface with the lines to be fully detected. We have to find another material, or texture, or field design to make it work properly.
With a cylinder
But then with a metalic cylinder, like this endmill, it works great. Even if the stepper motor is currently to slow to be reactive enough to follow quick movements.
Should we change the name of this pong machine to something else? We feel that we have created another game, a collaborative game :)
Which step have we reached?
We have found a low-tech way to detect the position of a ball and move a motor accordingly. Of course, we didn't get the end result we wanted, a fully functional pong machine, but we managed to create the basic tools to make it work. And we are super happy with it.
What is to be done?
First the metal ball we are using is only detected once out of 5. We think that is due to the fact that the ball is a sphere, and therefore has a very small contact to close the circuits between the two lines - theoretically just a point.
There are different solutions for that. Either we bring the lines of copper even closer (we have tested with laying out copper tape and simply cutting it in half in length with a cutter, therefore just creating a gap between both), or on the contrary bringing them a tiny bit further apart so that the ball can sit in the ridge created between them and allowing for two contact points. It might make the ball bounce a bit, or even getting stuck, so we’ll have to see.
Secondly the motor doesn’t move very fast and the ball can easily cross to columns without the motor catching up. A solution would be to move the motor off the rail, therefore having less weight to move around. It might not be good enough and we might just need a faster motor. To study more.
Thirdly, we haven’t actually gotten the solenoid to work. It might hit the ball to hard, it might miss it - hit too early or too late.
Extra ideas
Have lines in the length of the board to calculate speed
Only have one "player" and the other side the ball just bounces against the wall
That wall could be slid closer or further away from the rail to change the level of hardness