Before we get started, I highly recommend going here to read the article. It's my original site so the quality is much better, and you have the opportunity to support my work if you choose.(Although it is completely free). If an option pops up offering you to subscribe, just simply hit "No thanks" if you'd prefer not.
A Quick PreviewFor this project, we will be using E.R.A., the Everything Robotic Arm we made in our previous project to execute each move. This is because ERA has a huge range of tion and implemented inverse kinematics.
Of course, this comes with new hardware as well as some CAD changes to the robotic arm’s design, but we’ll get into that later. If you haven’t already and would like to build/learn about this robotic arm, check out this post. It’ll walk you through everything you need to do to build your very own and understand the basic concepts. This article is about upgrading it into the accurate, efficient, confidence wrecking machine you see here.
The Chess MatchI went ahead and put the actual matches here, near the top, since I figure you’d want to know what you’re getting in to. Although, it would certainly be more satisfying to understand how the robot works first then come back! But you do you ;D.
This is Alex. He’s a chess instructor from Seattle and will be playing against our bot today. In the first match, he will be competing against the Micro-Max engine. In the second, he will be playing Stockfish (engines explained below).
We know each other well, so I promise I’m not being cruel by having the robot verbally roast him every third move in the first match. 😂
Full first match:
(Note: In this match all Alex has to do it tell the robot what his move is. Everything else is automated. In the second/Stockfish match, Alex has to relay the moves calculated by the engine on the computer.)
Full second match:
I wont spoil what the actual results of the matches were. (You’ll have to see for yourself! Or, I guess just skip to the end of the video). But it should come as no real surprise what happened.
Regardless of the results of the first match, it’s a win so long as the robot did a good job listening to whatever the engine was telling it. Which it did! As long as the robot works correctly, it’s still a victory.
Obviously, Stockfish is insanely powerful (and can beat anyone in chess) so it’s not much a surprise what happened in the second match either and shows the potential of the robot. It didn’t feel as satisfying having to manually relay the data, but the robot still did a great job.
I also understand that the motors are a bit loud. This is because I’m cheap. If you decide to do the build, some nicer stepper drivers would fix that right up!
It takes a lot to make these projects and distribute them for free. If you enjoy my work, please consider becoming a free or paid subscriber on my original homepage. It helps me get noticed and is a great way to show support. Plus, it's completely free as well as contains many ofmy other projects! If a pop up appears, just simply click "No thanks" at the bottom". Thank you, and lets get back to the build!
The basic idea behind this robot is that a chess engine will send it coordinate positions where to move on a chess board, than the robotic arm will execute a sequence of moves to grab the piece securely, and place it at the next position sent to it by the chess engine.
To go in more detail, a microcontroller will send the robot a position from [1, 1] - [8, 8] detailing which piece it needs to move toward on the chess board through serial communication. The first number represents the row, and the second represents the column. For example, if it were to send [2, 4] the robot would know to move to the second row, and fourth column to grab the piece.
You may be wondering why the eight is where the zero should be. There is actually a very important reason for this. The integer zero is the default state of the data that we are sending over serial communication. If we put zero as a board position, then the robot would try to move there even when it shouldn’t. That would lead to problems, so I just made that position eight.
You may now be wondering, “Why not just shift the board down so that one is one then number it through eight?” Well, in order for the robot to know where to move to be at whatever position of the chessboard, each position needs to correspond with coordinate positions relative to the location of the robotic arm. For example, if I told the arm to move to [4, 7], the robot would go to the position [4, 7] in a 3D array (imagine a list containing smaller lists), then pull out the x, y, and z coordinates of that position in the array. The robot would then move to those x, y, and z coordinates through the power of inverse kinematics, and voilà! It’s at the piece!
From there, all the robot does is move down to grab the piece (change y-coordinate), move back up to avoid hitting other pieces, and move back to the home position coordinates.
If you’re wondering why it cannot be positions such as that on a normal chess board like ‘b1c3’, it’s because it’d be much harder to tell a robot what to do with it. They’d each have to be individual characters, then strung up, translated, and separated to array positions. It’s just easier for them to start as array positions.
The chess engine used to compute each move in the first demonstration is the Micro-Max (µ-Max) engine by H.G. Muller. I decided to use this chess engine because it has a allegedly similar Elo rating (level of skill) to the chess instructor I’m facing it off against (about 2000), as well as is known for being extremely compact. Being that it is about 2KB, it means I can run it on a microcontroller.
I wanted to do it this way because it is way simpler to configure as well as include built in communication between the engine and robot. This being said, the microcontroller is actually actively communicating with the robot. I’ll explain how this is done later in the programming. You could do this with a more complex engine, but it involves writing a script to isolate the moves computed by the engine, then sending it through serial via an actual computer or something with a lot more processing power.
That being said, the robot is not automatically communicating with the stockfish engine in the second match. Alex will just be manually relaying the data from the engine running on my computer and inputting it into the robot live. If I get enough support for this, I’ll probably automate it in a future project as well as create a computer vision system to automatically record each move, making it completely autonomous.
The brains of this project consist of two ESP32-S3 microcontrollers. One to run the Micro-Max engine as well as play some funny ChatGPT-generated phrases, while the other one is tasked with computing all of the motor movements and the physical execution of grabbing and placing pieces. All the sounds I’m playing are recorded on a voice playback module. There are also three LM2596 voltage regulators to step down the voltage for the servo motors and microcontroller. The stepper motors are controlled by TB6600 stepper drivers.
Setting Up ERA’s HardwareThe set up for the ESP32-S3 in charge of moving the robotic arm is below.
Of course, I will need a PCB to mount all of the components to, as well as to ensure that everything is connected correctly.
Fabrication file for the new board: Here
(Click “view raw” to download).
We can order these through a PCB manufacturer. As usual, I chose PCBWay which just had their 9th anniversary!
They have great prices (only $5) and their entire process is very simple and streamline. All I had to do was go to PCBWay.com, click on quick-order PCB, and upload the Gerber file for the new board. Or alternatively, just go here which I have saved in my favorites bar.
When the boards got here a few days later, as usual, the quality was fantastic!
With a quality PCB acquired, we can get back to the build!
NOTE:If you order your ESP32-S3 through Teyleten, the boards are slightly thicker than the official boards from Espressif. This PCB is designed to fit the official boards, but you can still make it work.
To wire up all of the stepper drivers and servos, check out this post.
Something else important to keep in mind is that if you power up the ESP32-S3 through serial and external power it could overheat the microcontroller. I left my ESP32-S3 regulator unattached until I was completely done.
Wiring Up Micro-Max and The Voice SystemThe wiring for this ESP32-S3 is pretty simple. All you have to do for hooking up the serial communication for Micro-Max is put a wire on GPIO17 & 18. Then, plug 17 into RX on the PCB/Smoothboard, and 18 into TX. This is because GPIO17 & 18 on the ESP32-S3 are the built in serial one pins. We can connect them by plugging TX on one to the RX on the other, and RX on one to the TX on the other.
For hooking up the sound, just plug GPIO4 on the ESP32-S3 to IO0, GPIO5 to IO1, GPIO6 to IO2, and GPIO7 to IO3. Then, attach the “busy” pin to GPIO10 and apply power.
The busy pin is awesome because it will be LOW when the voice chip is actively speaking and HIGH when it’s not. We can read the state of this pin to know when it’s time to disable the voice.
A more clear visual can be found here.
Note, however, that in the picture all three switches are ON (the “ON” “DP” pins). For our circuit, we will want them all OFF so we can give them binary inputs to play the desired files.
To attach the speaker, just connect the wires from the speaker output directly to any 4 Ohm, 3-5W speaker. I got this one and haven’t had any problems.
The basics of how to operate the voice chip is as follows:
A HIGH state is the default for the chip. This means if we want to play a file, we need to send a binary number to the chip through LOW states. For example, we currently have four pins connected to the chip, I chose four because I only have 11 audio files that need to be played and four binary digits can go up to 16 (2^4). If we want to play file one on the chip, we will write it a one in binary: 0-0-0-1. If we want to play the file two we will write a two in binary: 0-0-1-0. If we want to play the file three we will write a three in binary: 0-0-1-1, and so on. And again, this is done by digitalWriting LOW states. (If we wanted to play file nine, the binary would be 1-0-0-1, meaning the first GPIO would be LOW, middle two are HIGH, and last one is LOW).
To load whatever voice files you desire onto the chip, just find whichever MP3 file you want to play online (I used FreeTTS to generate the speech files that ChatGPT Inspired), then connect the chip to your PC via micro-USB and drag the MP3 files over to the device. Then, rename the files as 0001 for one, 0002 for two, 0010 for ten, 0100 for one-hundred, 0255 for two-hundred and fifty-five, etc.
Then boom! You’re done.
My full setup for the rest is below:
You may see that there’s an additional ESP32 on the far left. This is not needed. I’m just using it to supply a clean 5V to the voice chip and speaker. Batteries or anything else would work just fine. Just be sure they have a common ground!
You may also see that there’s a cool little case holding up our PCB. You can download this for free here at Thingiverse with the other CAD for the project.
Something to note for the gripper, is that the grip padding is actually just some one inch furniture pads (doesn’t have to be those exact ones). They’re cheap and have worked great so far.
Something else really neat about this gripper is that, if you noticed, the plastic actually flexes a bit when you grab a piece. This is awesome because it means the motor wont stall/overheat. This is due to the properties of the 3d-printed plastic. Since the servo motor is still able to move when grabbing a piece, the current draw remains low in the motor while a strong grip force builds up proportionally to the elasticity of the plastic. Pretty cool, right?
Robotic Arm CAD ChangesI’ve introduced three changes to ERA since my original post. They’re slight, but important.
The first change I’ve made was that I’ve added a spot to mount a cooling fanto the J2/second joint motor. This is important because, since that motor is so small, it would often heat up enough to melt the plastic and deform the robot under continuous operation. I also attached a few heat sinks to it to help dissipate the heat and so far, I’ve had zero problems.
The second change I made was also involving that motor. I’ve upgraded the gearbox from the original 5:1 to a 14:1 gearbox. This is to help the robot in lifting heavier payloads. Although a chess piece isn’t a heavy payload, I wanted to add it anyway so it could have a easier time lifting up other things that I may need it for. This also means that the code for this robot has been updated for a 14:1 gearbox. If you’d prefer to keep it as a 5:1, change this line:
j2Stepper.moveTo(theta2*137.335640);
to this:
j2Stepper.moveTo(theta2*51.8);
That’s all for now on the CAD updates!
The new CAD has been updated on the original design on Thingiverse.
What I’d ImproveObviously, this robot is not perfect. There was a lot of work that went in to making it functional, but even that’s not quite enough.
The robot would skip steps sometimes, meaning I would have to re-home the robot. It would sometimes miss the piece due to error in what could be many different areas, including the inputted piece coordinates, inverse kinematics calculations, step calculations and many other things. The robot would also get quite hot at times, which if left unsupervised could melt the plastic or damage the motors.
If I were to do it again, these are some things I would add/change:
- Magnetic encoders on every axis.
- Brushless DC motors for the robotic arm instead of stepper motors.
- More efficient cooling system.
- Better playing surface/coordinate documentation.
- More complex gripper that is thinner to reduce likelihood of hitting another piece.
- Things I expressed earlier such as a vision system for the board and built-in Stockfish script.
I think with these changes, the robot would be much more reliable and require less maintenance. But there just wasn’t enough time for everything.
There’s a very high chance I will make a ERA version 2 in the future with some of these changes to the actual robotic arm, but it would be up to you readers if I were to implement it with chess. Let me know your thoughts in the comments on my homepage.
The ProgrammingNow I’ll go through the programming that makes this project work as well as explain the logic behind it. This project is done in the Arduino IDE. If you’re not into the programming, no worries. Feel free just to scroll past it.
ERA code file: here
Micro-Max code file: here
This will be the code for the ESP32-S3 controlling the robotic arm.
First things first, we declare the two libraries we’ll be using.
#include <AccelStepper.h>
#include <ESP32Servo.h>
Next, we declare all the different global variables.
float theta1;
float theta2;
float x;
float y;
float z;
float delta;
float theta3;
float psi = 180; //Desired gripper orientation. Offsetting for servo tolerance
const int wristPin = 2;
const int gripperPin = 1; //Was 38
const int gripPitchPin = 8; //Was non existent
const int numRows = 8;
const int numCols = 8;
const int depth = 3;
const float timeToTarget = 3;
unsigned long prevMillis = 0;
int serialNum = 0;
int j1Speed;
int j2Speed;
int baseSpeed;
float j1MaxDis = 0;
float j2MaxDis = 0;
float baseMaxDis = 0;
int reqRow = 0;
int reqCol = 0;
int gripperNum = 1;
int gripperCount = 0;
bool piece;
bool down;
bool up;
bool grab = true;
Now we can create the 3D array that holds all the coordinates of each chess piece.
int chessBoard[numRows][numCols][depth] =
{
{{248, -267, 164}, {245, -268, 116}, {243, -269, 69}, {241, -270, 19}, {239, -271, -27}, {237, -272, -75}, {235, -273, -122}, {233, -274, -170}},
{{297, -271, 164}, {295, -272, 115}, {293, -272, 68}, {291, -273, 20}, {288, -274, -28}, {286, -274, -77}, {284, -275, -124}, {282, -276, -172}},
{{346, -275, 163}, {344, -276, 114}, {342, -275, 67}, {340, -276, 22}, {337, -277, -29}, {335, -276, -79}, {333, -277, -126}, {331, -278, -174}},
{{394, -379, 162}, {392, -280, 113}, {390, -279, 66}, {388, -278, 23}, {385, -280, -31}, {383, -279, -80}, {381, -279, -127}, {379, -279, -175}},
{{443, -283, 162}, {441, -283, 112}, {439, -282, 65}, {437, -281, 23}, {434, -283, -32}, {432, -281, -82}, {430, -281, -128}, {428, -281, -176}},
{{492, -287, 161}, {490, -287, 112}, {488, -285, 64}, {486, -284, 24}, {483, -285, -33}, {481, -283, -83}, {479, -283, -130}, {477, -282, -178}},
{{541, -292, 160}, {539, -291, 111}, {537, -289, 63}, {535, -288, 25}, {532, -287, -34}, {530, -286, -83}, {528, -285, -132}, {526, -283, -180}},
{{586, -295, 162}, {590, -297, 118}, {590, -297, 70}, {590, -297, 22}, {590, -297, -26}, {590, -313, -74}, {590, -297, -122}, {552, -317, -176}}
};
Mine looks like so, however, yours will likely be different since you’ll probably be using a different chess board. Finding these coordinates can be challenging, and normally aren’t ever perfect since the robot will always have some error without encoders. My recommendation is to find the coordinates of each edge piece through some trial and error then use linear interpolation to find all of the pieces between them.
This isn’t as bad as it sounds. If I told you I needed to find three numbers with the first being 5, and the last being 15. You’d probably guess the middle one is 10. You just do this but with more numbers. Division is your friend!
Next, we need to create an object for all of our motors.
Servo wrist; //Create servo object to control a servo
Servo gripper; //Create servo object to control a servo
AccelStepper baseStepper(1, 5, 4); //(Type:driver(1 is default driver), STEP, DIR)
AccelStepper j1Stepper_L(1, 7, 6); //(Type:driver(1 is default driver), STEP, DIR)
AccelStepper j1Stepper_R(1, 16, 15); //(Type:driver(1 is default driver), STEP, DIR)
AccelStepper j2Stepper(1, 10, 9); //(Type:driver(1 is default driver), STEP, DIR)
Then, as required by the Accelstepper library, we need to declare max speeds of each motor. I just chose 2000 since the speed would never get that high. While we’re at it, we can attach the servos to a pin.
baseStepper.setMaxSpeed(2000); //400 pulse/rev
j1Stepper_L.setMaxSpeed(2000); //400 pulse/rev
j1Stepper_R.setMaxSpeed(2000); //400 pulse/rev
j2Stepper.setMaxSpeed(2000); //400 pulse/rev
wrist.attach(wristPin);
gripper.attach(gripperPin);
Then we begin serial communication. “Serial” Is the default one, and “Serial1” is the second set of pins that we will use to get our moves from. You have to declare the data type as well as which pins TX and RX are on.
Serial.begin(115200);
Serial1.begin(115200, SERIAL_8N1, 17, 18); //(TX, RX)
Now we call some functions to prep the robot. What moveGripper does is open the gripper if the input is 0, and close it if 1. ResetDis just sets the max distance variables back to zero. This is important for a speed function we’ll get into later. Then finally, the goHome function which just sets the x, y, and z coordinates to whatever we want the home position to be.
moveGripper(0);
resetDis();
//Start position
goHome();
With the robot now ready to start, we need to start listening for any Serial1 inputs that are being sent from the other controller. When we get an number, we can isolate the 10s and 1s digit through division and use of the modulus operator. If we want to do manual input such as for the Stockfish engine, we just change Serial1.read() to Serial.parseInt() and Serial1.available() to Serial.available().
if (Serial1.available() > 0)
{
serialNum = Serial1.read();
if (serialNum > 0)
{
resetDis();
reqRow = serialNum / 10;
if (reqRow == 8)
reqRow = 0;
reqCol = serialNum % 10;
if (reqCol == 8)
reqCol = 0;
if (reqRow != 7)
moveToPiece(reqRow, reqCol);
}
}
With the coordinate position identified, we just complete a series of moves to get to the piece, grab it, move back up, and go home. This can be done through a else-if chain.
else if (atPiece())
{
down = true;
moveDown(90);
}
else if (down && atPosition())
{
moveGripper(gripperNum);
changeGripperNum();
delay(1000);
up = true;
grab = false; //Dont update gripper while moving up
moveUp(90);
down = false;
}
else if (up && atPosition())
{
resetDis();
goHome();
grab = true;
up = false;
}
If the atPiece function is true, we know it’s above the desired position. Then we can call the moveDown function which just increases the x-coordinate by however much the input is. (Remember, positive is down!) The moveUp function just does the opposite. We can ensure that the order of operations moves down the list by using Boolean variables as shown. By making each negative after the operation is complete, we can ensure it doesn’t hit it again until after everything has been executed. Also, all that the changeGripperNum function is doing is alternating the gripper state by changing it to a 1 if it’s a 0 and a 0 if it’s 1. This makes it so that the gripper closes at the first piece it reaches and opens at the next, then it repeats.
Now we call our inverse kinematics function to calculate each theta value that the robot needs to move to. I’m not going to explain it here since it’s already done in this post. Just remember, the robot needs to be completely vertical at the start to work correctly!
inverseKinematics(x, y, z);
To move the gripper correctly, we need to multiply the theta3 value by.67. This is because I had to scale the 0-180deg write value to 0-270, since a MG996R servo has a 270 degree range of motion. We also only want the gripper to move when it’s away from a piece, so we introduce a Boolean to be false when actively grabbing a piece.
if (theta3 > 0 && grab)
wrist.write(theta3 * .666667);
Next up is some pretty important code. This code will translate the calculated theta values from our inverse kinematics function into steps, while also finding the relative max distance from the target position. I wont explain the translation part since I did it here, but the point of finding the max steps is so that we can calculate the motor speed so that we can ensure all of the motors arrive to their position at the same time. This is nice because it means that the gripper will move in a straight line when approaching a piece, ensuring no other pieces fall over. It also makes it so that all the joints will arrive in a set amount of time which is helpful in calculating the delay in the Micro-Max code.
NOTE: In the videos for the match I actually just updated the code for constant speed movements. This is because when using the calculated values, the robot would sometimes skip steps. This is very unideal when playing a long chess game. To me this was a worth trade-off, even if the motions weren’t as smooth.
baseStepper.moveTo(delta*16.666667);
if (abs(baseStepper.distanceToGo()) > baseMaxDis)
baseMaxDis = abs(baseStepper.distanceToGo());
baseSpeed = baseMaxDis/timeToTarget;
Now we can just introduce a function to slow the motors down by 25% if within 10% of the target position. From there we just set the motor speed and do this a few more times for the rest of the motors. It’s all the same thing.
if (isClose(baseStepper, baseMaxDis*.1))
baseSpeed *= .75;
baseStepper.setSpeed(baseSpeed);
Then, we just run each motor until it reaches the target position set by the moveTo function above.
baseStepper.runSpeedToPosition();
j1Stepper_L.runSpeedToPosition();
j1Stepper_R.runSpeedToPosition();
j2Stepper.runSpeedToPosition();
That’s it!
This will be the code for ESP32-S3 with Micro-Max:
I’m only going to be going over the changes I made to set up communication, since I have no clue how the actual chess engine works. This is because the program consists of code obfuscation. I think this is just to compress it, but I’m unsure. Anyway, here we go!
The first thing I did was introduce some new variables. An array to hold the new value we would send to the robot, a few Booleans, and some pins for the audio.
int toInt[4];
bool audio1;
bool onlyOne;
int randomNum;
const int audioPin1 = 4;
const int audioPin2 = 5;
const int audioPin3 = 6;
const int audioPin4 = 7;
const int audioLine = 10;
unsigned long audioCount = 0;
With that, we begin serial communication and set the pin type for all of our audio pins. We also need to call the function prepAudio, which just sets all the pins HIGH since that is the default state on the voice chip we’re using.
Serial.begin(115200);
Serial1.begin(115200, SERIAL_8N1, 17, 18); //(TX, RX)
audio1 = true;
pinMode(audioPin1, OUTPUT);
pinMode(audioPin2, OUTPUT);
pinMode(audioPin3, OUTPUT);
pinMode(audioPin4, OUTPUT);
pinMode(audioLine, INPUT_PULLDOWN);
prepAudio();
Then, in the loop, we call this super magical function I made to translate the normal ‘d2d4’ chess values into coordinates on an array. All I do to make this work is a huge, brute-force, else-if chain which compares the character values to their ASCII counterparts. If they do in fact equal, put that position in the new array we made to hold all of the new coordinate values.
translateForERA();
Now we can just add them to two integers which will hold the position we need to grab at, and the position we need to place at.
int grab = (toInt[0] * 10) + toInt[1];
int place = (toInt[2] * 10) + toInt[3];
Since the robot needs to wait for new serial input to move to the next piece, we introduce a delay to hold the program until the robot is ready. Since the speed code we made makes it take three seconds to reach the position, we can calculate the desired delay time. While doing this, we can also generate a random number to play whatever audio file.
int timeDelay = (3 * 3000) + 1000 + 1000;
randomNum = random(1, 11); //Decide which audio to play (1-10)
At the start of the game, we want a specific audio to be played so we use a Boolean to keep track of it. Then after that, we want to play a audio every third turn. We can do this with the playRandomAudio function, which just outputs a Boolean number of pin states that equal whatever the random number is.
if (audio1) //Plays game start audio
{
digitalWrite(audioPin1, LOW);
delay(500); //Give the chip a half sec to send the signal
while(digitalRead(audioLine) == LOW) {};
audio1 = false;
}
else if (audioCount % 3 == 0) //Plays random audio every other turn
playRandomAudio();
When that’s done, we want the audio to be silence. I managed to get this to work by uploading a empty MP3 file to be played after the original sound file. This is to compensate for hardware limitations, since the BUSY pin will remain LOW a little longer than it should. We also need to increment the audio count so that we can make sure audio only plays on every other turn.
silence(); //Plays file 11, which is a blank file.
audioCount++;
Now, we just send the grab number to the robot, wait the calculated amount, send the place number, and reset the audio.
Serial1.write(grab);
delay(timeDelay); //Time delay
Serial1.write(place);
prepAudio();
That’s it, again!
Thanks so much for reading! I hope this was a helpful and informative article. If you decided to do the build, please feel free to leave any questions in the comments. If not, I hope you were still able to enjoy reading and learn something new!
If you'd like to support my work, please consider becoming a free or paid subscriber on my homepage. It really goes a long way, and is completely free! If a pop up appears, just simply click "No thanks" at the bottom".
Have constructive criticism? I’m always looking to improve my work. Leave it in the comments! Until next time.
Instagram: RoboticWorx
Comments