If you’re looking to combine your love for gaming with a knack for tinkering, you’ve landed in the right place. Today, we’re pairing a game controller with an ESP32 using Bluetooth. That way, you can control all kinds of projects like robots, robot vehicles, animatronics, motorized props, or even cooler, a battle bot!
In order to get the ESP32 talking to my PS4 game controller, we’ll use an Arduino sketch to tap into the ESP32’s Bluetooth capabilities. This connection will later allow us to use the gamepad’s buttons and joysticks to control LEDs, motors, servos and more for our projects.
As for the Arduino code itself, there are a variety of Arduino IDE libraries that make it easy to connect your game controller to an ESP32 using Bluetooth. Some libraries are specific to certain game controllers, while others are more universal and allow you to connect a variety of controllers. The one we’re going to be using is Bluepad32. It supports most, if not all, modern Bluetooth game controllers, mice and keyboards. Not sure if you’re game controller is compatible? Check out the current list of Bluepad32-compatible controllers as well as what features are supported in the docs.
All right, you know what time it is – to the code!
Setting up the Arduino IDE
Fire up your Arduino IDE! Once that blank sketch comes up, there are two …I call them pre-setup steps …that we’re going to have to go through. This means you can’t just scroll to the bottom of this ESP32 game controller tutorial and simply copy and paste the code!
It won’t work without first completing these steps:
1. Install ESP32 & Bluepad32 Board Managers
The Arduino IDE doesn’t come with support for ESP32 boards by default like for the rest of the Arduino microcontroller product line. But we can easily add it by installing board managers for both the ESP32 and Bluepad32. We can do this with just a few clicks right in the Arduino IDE!
Go to File => Preferences (Win) or Arduino IDE => Preferences (Mac)

In the Preferences popup window, look for Additional Board Manager URLs towards the bottom. Just to the right of the text field is a button to expand that option. You’ll see a larger text field where you’ll be copy and pasting these 2 urls:
- ESP32 Boards Manager: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
- Bluepad32 Boards Manager: https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
Be sure to copy and paste each url into its own line. The first one is for installing the boards manager for the ESP32 and this second one right below that is for installing the Bluepad 32 boards manager.

Click OK to close the Additional Boards Manager URLs expanded view. And then click OK again to close the Preferences popup window.
2. Add ESP32 & Bluepad32 Board Packages
With the two new boards managers installed, let’s now add the actual boards. Just a few more clicks and we’re there!
Go to Tools => Board => Boards Manager
In the text box at the top of the left column, start typing in esp32 and you’ll see some board packages come up below.

The one you want is called esp32 by Espressif Systems.
Click Install. It only takes a few moments for the installation to complete.
I already have it installed so my button appears as REMOVE.
After that, let’s do the Bluepad32 boards package. Back in the text box above, just continue adding bluepad32 and you’ll see esp32_bluepad32 by Ricardo Quesada come up.

Click Install and wait a bit for the installation to finish up.
That’s it – our two pre-setup steps are done! All we have left to do is to select the board we’re using and the port that the ESP32 is plugged into.
Select the Correct ESP32 Development Board and Port
The Bluepad32 library comes with a few useful examples to help get us started, but in order to access them, we have to tell the Arduino IDE exactly what ESP32 development board we’re working with.
There’s a nearly endless variety of these development boards out there and the best way to find out exactly which one you have is to thoroughly read your ESP32 product listing and documentation.
I’m using this 30-pin ESP-WROOM-32 ESP-32S which is widely available and inexpensive.
Go to Tools => Board => esp32_bluepad32
You’ll be presented with a huge list of ESP32 development boards to choose from! In my case, I selected the DOIT ESP32 DEVKIT V1.

If you’re still not sure what development board you have, try this option first because it’s a really popular board. If you bought yours on places like Amazon or another inexpensive outlet, this is probably the one you have.
After that, let’s select our port.
Go to Tools => Port
Mine happens to be COM6. If you’re not sure what port to select, try unplugging other devices from your computer’s USB ports so that all you have left still plugged in is the ESP32. The port that still shows up in the Port menu is your ESP32 port!
Alright, all the boring stuff is done!
Bluepad32 Controller Example Sketch
Before we can start controlling motors, servos and more with our game controller, we first have to figure out how the buttons and joysticks are handled by the Bluepad32 library.
And thanks to one of the built-in example sketches, we can do this without typing a single line of code!
Go to File => Examples => Bluepad32_ESP32 => Controller
If you are a total beginner this Arduino sketch can seem pretty overwhelming. But the good news is that there’s only three main sections that we have to worry about:
1. Reading Game Controller Values
Scrolling down the example sketch, look for this code block called void dumpGamepad(ControllerPtr ctl) starting around line 44:

This block of code prints to the Serial Monitor all the values that your game controller buttons and joysticks output when they are pressed or moved around. That way, we can assign functions to these values like turning on LEDs with a button press, or accelerating motors and moving servos with a joystick.
Because this Arduino library supports so many different kinds of control devices, not all of this information will apply to my PS4 game controller. The ones we want to pay attention to for your typical gamepad is:
- ctl->dpad(): Values for the D-pad buttons
- ctl->buttons(): Values for all the other buttons including trigger
- ctl->axisX(): Left joystick values when you move it left and right (X axis)
- ctl->axisY(): Left joystick values when you move it up and down (Y axis)
- ctl->axisRX(): Right joystick values when you move it left and right (X axis)
- ctl->axisRY(): Right joystick values when you move it up and down (Y axis)
2. Processing Gamepad Actions
Continue scrolling down the Controller example sketch so we can take a look at the second notable section. Right around line 128, you should see the void processGamepad(ControllerPtr ctl) code block:

This section is where you’d normally put all the code to activate components on your project using the game controller. There are three examples in here for us to test out, some of which may or may not be compatible with your particular game controller:
- ctl->a(): If you press the “a” button on your gamepad, it will toggle between three light bar colors on your controller – red, green and blue. My PS4 controller doesn’t have an “a” button, so we’ll have to see which button this corresponds to once we upload this sketch.
- ctl->b(): If you press the “b” button, it allows you to select your “player seat” or “player color”. Normally, this is set when a player pushes the center PS4 button.
- ctl->x(): If you press the “x” button, then it will activate the rumble feature if your gamepad supports it. Keep in mind that the “x” button in the code may not correspond to the X button on your game controller.
There are a few different ways to specify actions for buttons with the Bluepad32 library and it just comes down to personal preference. I’m going to show you how I go about determining which button is which after we upload this Controller example.
3. Setting Up Components
The final code block that you have to pay attention to, and this is going to take some scrolling, is the setup loop.
The void setup() is a familiar part of every Arduino sketch but I wanted to point it out because it appears at the very bottom of the Controller example and is easy to miss! It’s around line 247:

Many electronic components need certain attributes to be included in this section in order for them to work later on in the code. This can be setting up pinModes, initializing components on pins, or any other functionality that only needs to be executed one time. Simply leave the existing code in there and just add to it for your project.
How to Pair Your Game Controller to the ESP32
Let’s upload this code as is and see what happens!
Once the upload process is complete (which takes longer on ESP32 boards than Arduinos), pop open the Serial Monitor. Before you can pair your game controller, reset the ESP32 board by pressing the tiny EN button on it.

This causes some information about your Bluetooth connection to appear on the Serial Monitor. And now you can pair your game controller.
As soon as the pairing is successful, you’ll see all kinds of numbers scrolling up the Serial Monitor.
How to Identify Button & Joystick Values
All the values you see coming up in the Serial Monitor are coming from your game controller thanks to one of those important code blocks I showed you: the dumpGamepad(ControllerPtr ctl).

It certainly lives up to its name! Let’s see if we can start sorting through some of these values that are being “dumped” out. Since the numbers are scrolling up, the most recent values will appear on the bottom row. This is where you want to watch.
PS4 D-pad Button Values
Although the D-pad consists of buttons too, those values appear as a separate column in the Serial Monitor. When none of the D-pad buttons are pressed, the value is 0x00.

One by one, start pressing and holding each button to see what unique value is assigned to each. For my PS4 controller I got:
- D-pad Up: 0x01
- D-pad Down: 0x02
- D-pad Left: 0x08
- D-pad Right: 0x04
Action and Trigger Button Values
For all the rest of the button values, we’re going to be looking at the next column over in the Serial Monitor. The default value is 0x0000 when nothing is being pressed.

Like you did earlier, press and hold each button to see their individual values. Here’s what I got for my PS4 controller:
- X: 0x0001 (also ctl->a(), which changes the LED light bar color)
- Square: 0x0004 (also ctl->x(), which activates the rumble feature)
- Triangle: 0x0008
- Circle: 0x0002
- R1 Trigger: 0x0020
- R2 Trigger: 0x0080
- L1 Trigger: 0x0010
- L2 Trigger: 0x0040
Take this opportunity to map out all the buttons and jot these hex numbers down. Later on, you’ll be using them to assign different actions to the components you use in your project.
Joystick Values
Jumping to the next set of columns labeled axis L and axis R, you’ll notice each one consists of two values separated by commas. This is where the values for our left and right joysticks will appear.
The first value of each pair is for the X axis (left and right) and the second is for the Y (up and down).

Joystick Idle Values
With the joystick at idle, I’d expect a value of 0 (zero) for both axes.
But even without touching any of the joysticks at all, you’ll notice both of these numbers fluctuating a bit. The range of numbers you get will be close to 0 (either positive or negative) but almost never exactly 0.
It’s extremely important to write the range of numbers you’re getting for both the X and Y when each joystick is at idle. Include whether the numbers are positive or negative.
I imagine that when the joysticks are at idle, you want your project to be in a “resting” position. If you just use 0 for your joystick idle values, then you better have some Holy water ready! As the numbers fluctuate, it will cause your project to make some unexpected movements and act on its own as if possessed!
The values for my joysticks at idle were:
- Left Joystick X: -12 to 16
- Left Joystick Y: -8 to 4
- Right Joystick X: -8 to -4
- Right Joystick Y: 16 – 20
Joystick Left, Right, Up and Down Values
Looking back at the dumpGamepad(ControllerPtr ctl) code block, the author gives us a clue as to what values to expect as we move around each joystick.

When we push either joystick to the left, the corresponding value for the X axis should go down to -511. Then, pushing it to the right should increase the X value to 512.
The same is true for thumbing up and down. When a joystick is pushed all the way up, the corresponding value for the Y axis for that joystick should dip to -511 and then increase to 512 when pushed all the way down.

Given what we’ve already seen with the idle values, I don’t expect these to be an exact match. Let’s see what really happens!
These are the values I got for each joystick on my PS4 controller when pushing all the way in each direction:
- Left Joystick X – left: -508
- Left Joystick X – right: 512
- Left Joystick Y – up: -508
- Left Joystick Y – down: 512
- Right Joystick X – left: -508
- Right Joystick X – right: 512
- Right Joystick Y – up: -508
- Right Joystick Y – down: 512
The negative limits for both joysticks didn’t quite make it all the way to -511, but at least the values were consistent at -508.
How to Use Button & Joystick Values
So now that we have all these values scribbled down on a sheet of paper, how do we use them?
As we saw in the Serial Monitor, when you press a button or move a joystick, its value changes. So the idea is to assign actions – like turning on LEDs, triggering sounds, accelerating motors or moving a servo – to the gamepad controls we want, based on specific values.
Game Controller with ESP32 Arduino Code Template
I took the Bluepad32 Controller example and created a starter template with a code block for every button and joystick on the PS4 controller. If you’re using another game controller, the values are easy to change to match your button and joystick values.
All you have to do is choose which buttons you want to use and drop in the code to activate your components within that code block. The same is true for each of the joysticks.
#include <Bluepad32.h>
ControllerPtr myControllers[BP32_MAX_GAMEPADS];
// This callback gets called any time a new gamepad is connected.
// Up to 4 gamepads can be connected at the same time.
void onConnectedController(ControllerPtr ctl) {
bool foundEmptySlot = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == nullptr) {
Serial.printf("CALLBACK: Controller is connected, index=%d\n", i);
// Additionally, you can get certain gamepad properties like:
// Model, VID, PID, BTAddr, flags, etc.
ControllerProperties properties = ctl->getProperties();
Serial.printf("Controller model: %s, VID=0x%04x, PID=0x%04x\n", ctl->getModelName().c_str(), properties.vendor_id, properties.product_id);
myControllers[i] = ctl;
foundEmptySlot = true;
break;
}
}
if (!foundEmptySlot) {
Serial.println("CALLBACK: Controller connected, but could not found empty slot");
}
}
void onDisconnectedController(ControllerPtr ctl) {
bool foundController = false;
for (int i = 0; i < BP32_MAX_GAMEPADS; i++) {
if (myControllers[i] == ctl) {
Serial.printf("CALLBACK: Controller disconnected from index=%d\n", i);
myControllers[i] = nullptr;
foundController = true;
break;
}
}
if (!foundController) {
Serial.println("CALLBACK: Controller disconnected, but not found in myControllers");
}
}
// ========= SEE CONTROLLER VALUES IN SERIAL MONITOR ========= //
void dumpGamepad(ControllerPtr ctl) {
Serial.printf(
"idx=%d, dpad: 0x%02x, buttons: 0x%04x, axis L: %4d, %4d, axis R: %4d, %4d, brake: %4d, throttle: %4d, "
"misc: 0x%02x, gyro x:%6d y:%6d z:%6d, accel x:%6d y:%6d z:%6d\n",
ctl->index(), // Controller Index
ctl->dpad(), // D-pad
ctl->buttons(), // bitmask of pressed buttons
ctl->axisX(), // (-511 - 512) left X Axis
ctl->axisY(), // (-511 - 512) left Y axis
ctl->axisRX(), // (-511 - 512) right X axis
ctl->axisRY(), // (-511 - 512) right Y axis
ctl->brake(), // (0 - 1023): brake button
ctl->throttle(), // (0 - 1023): throttle (AKA gas) button
ctl->miscButtons(), // bitmask of pressed "misc" buttons
ctl->gyroX(), // Gyro X
ctl->gyroY(), // Gyro Y
ctl->gyroZ(), // Gyro Z
ctl->accelX(), // Accelerometer X
ctl->accelY(), // Accelerometer Y
ctl->accelZ() // Accelerometer Z
);
}
// ========= GAME CONTROLLER ACTIONS SECTION ========= //
void processGamepad(ControllerPtr ctl) {
// There are different ways to query whether a button is pressed.
// By query each button individually:
// a(), b(), x(), y(), l1(), etc...
//== PS4 X button = 0x0001 ==//
if (ctl->buttons() == 0x0001) {
// code for when X button is pushed
}
if (ctl->buttons() != 0x0001) {
// code for when X button is released
}
//== PS4 Square button = 0x0004 ==//
if (ctl->buttons() == 0x0004) {
// code for when square button is pushed
}
if (ctl->buttons() != 0x0004) {
// code for when square button is released
}
//== PS4 Triangle button = 0x0008 ==//
if (ctl->buttons() == 0x0008) {
// code for when triangle button is pushed
}
if (ctl->buttons() != 0x0008) {
// code for when triangle button is released
}
//== PS4 Circle button = 0x0002 ==//
if (ctl->buttons() == 0x0002) {
// code for when circle button is pushed
}
if (ctl->buttons() != 0x0002) {
// code for when circle button is released
}
//== PS4 Dpad UP button = 0x01 ==//
if (ctl->buttons() == 0x01) {
// code for when dpad up button is pushed
}
if (ctl->buttons() != 0x01) {
// code for when dpad up button is released
}
//==PS4 Dpad DOWN button = 0x02==//
if (ctl->buttons() == 0x02) {
// code for when dpad down button is pushed
}
if (ctl->buttons() != 0x02) {
// code for when dpad down button is released
}
//== PS4 Dpad LEFT button = 0x08 ==//
if (ctl->buttons() == 0x08) {
// code for when dpad left button is pushed
}
if (ctl->buttons() != 0x08) {
// code for when dpad left button is released
}
//== PS4 Dpad RIGHT button = 0x04 ==//
if (ctl->buttons() == 0x04) {
// code for when dpad right button is pushed
}
if (ctl->buttons() != 0x04) {
// code for when dpad right button is released
}
//== PS4 R1 trigger button = 0x0020 ==//
if (ctl->buttons() == 0x0020) {
// code for when R1 button is pushed
}
if (ctl->buttons() != 0x0020) {
// code for when R1 button is released
}
//== PS4 R2 trigger button = 0x0080 ==//
if (ctl->buttons() == 0x0080) {
// code for when R2 button is pushed
}
if (ctl->buttons() != 0x0080) {
// code for when R2 button is released
}
//== PS4 L1 trigger button = 0x0010 ==//
if (ctl->buttons() == 0x0010) {
// code for when L1 button is pushed
}
if (ctl->buttons() != 0x0010) {
// code for when L1 button is released
}
//== PS4 L2 trigger button = 0x0040 ==//
if (ctl->buttons() == 0x0040) {
// code for when L2 button is pushed
}
if (ctl->buttons() != 0x0040) {
// code for when L2 button is released
}
//== LEFT JOYSTICK - UP ==//
if (ctl->axisY() <= -25) {
// code for when left joystick is pushed up
}
//== LEFT JOYSTICK - DOWN ==//
if (ctl->axisY() >= 25) {
// code for when left joystick is pushed down
}
//== LEFT JOYSTICK - LEFT ==//
if (ctl->axisX() <= -25) {
// code for when left joystick is pushed left
}
//== LEFT JOYSTICK - RIGHT ==//
if (ctl->axisX() >= 25) {
// code for when left joystick is pushed right
}
//== LEFT JOYSTICK DEADZONE ==//
if (ctl->axisY() > -25 && ctl->axisY() < 25 && ctl->axisX() > -25 && ctl->axisX() < 25) {
// code for when left joystick is at idle
}
//== RIGHT JOYSTICK - X AXIS ==//
if (ctl->axisRX()) {
// code for when right joystick moves along x-axis
}
//== RIGHT JOYSTICK - Y AXIS ==//
if (ctl->axisRY()) {
// code for when right joystick moves along y-axis
}
dumpGamepad(ctl);
}
void processControllers() {
for (auto myController : myControllers) {
if (myController && myController->isConnected() && myController->hasData()) {
if (myController->isGamepad()) {
processGamepad(myController);
}
else {
Serial.println("Unsupported controller");
}
}
}
}
// Arduino setup function. Runs in CPU 1
void setup() {
Serial.begin(115200);
Serial.printf("Firmware: %s\n", BP32.firmwareVersion());
const uint8_t* addr = BP32.localBdAddress();
Serial.printf("BD Addr: %2X:%2X:%2X:%2X:%2X:%2X\n", addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
// Setup the Bluepad32 callbacks
BP32.setup(&onConnectedController, &onDisconnectedController);
// "forgetBluetoothKeys()" should be called when the user performs
// a "device factory reset", or similar.
// Calling "forgetBluetoothKeys" in setup() just as an example.
// Forgetting Bluetooth keys prevents "paired" gamepads to reconnect.
// But it might also fix some connection / re-connection issues.
BP32.forgetBluetoothKeys();
// Enables mouse / touchpad support for gamepads that support them.
// When enabled, controllers like DualSense and DualShock4 generate two connected devices:
// - First one: the gamepad
// - Second one, which is a "virtual device", is a mouse.
// By default, it is disabled.
BP32.enableVirtualDevice(false);
}
// Arduino loop function. Runs in CPU 1.
void loop() {
// This call fetches all the controllers' data.
// Call this function in your main loop.
bool dataUpdated = BP32.update();
if (dataUpdated)
processControllers();
// The main loop must have some kind of "yield to lower priority task" event.
// Otherwise, the watchdog will get triggered.
// If your main loop doesn't have one, just add a simple `vTaskDelay(1)`.
// Detailed info here:
// https://stackoverflow.com/questions/66278271/task-watchdog-got-triggered-the-tasks-did-not-reset-the-watchdog-in-time
// vTaskDelay(1);
delay(150);
}
Code Explanation
In this Game Controller with ESP32 code template, the main area to focus on for adding your own code to control components on your project is:
// ========= GAME CONTROLLER ACTIONS SECTION ========= //
As you scroll down this section, you’ll see where you can put your code to turn on and turn off components based on button press and joystick values.
Using Button Values
Here is an example for the PS4 controller X button:

if (ctl->buttons() == 0x0001) is the same as saying “if the button that is pressed is equal to 0x0001 (the X button value), then perform the actions within the curly brackets that follow.”
Then, the line of code below says, if (ctl->buttons() != 0x0001). This means “if the button value is not equal (!=) to 0x0001 (X button not pressed), then perform the actions within that set of curly brackets.”
This code format is repeated for every button of my PS4 game controller.
Using Joystick Values
Let’s look at a joystick example next! This block of code defines movements for the left joystick in particular:

The first scenario you see is when you push up on the joystick.
//== LEFT JOYSTICK - UP ==//
if (ctl->axisY() <= -25) {
// code for when left joystick is pushed up
}
In plain language, this code block says: “if the value of the Y axis is less than or equal to -25, execute the code within the curly brackets that follow.”
Recall that when you thumb up, the values of the joystick go from 0-ish at idle to -508 when pushed all the way up. Because of the fluctuating values at idle, I don’t want any components to activate until I thumb up past a value of -25. I chose -25 as a starting point because the most negative number I saw at idle was -18 for the left joystick Y axis. Always choose a number outside the idle fluctuation range.
This code format is then repeated for moving the left joystick in each remaining direction: down, left and right.
But what happens when the left joystick is at idle?
So far, we’ve defined joystick values for anything above 25 (right and down) and below -25 (left and up). But what about joystick values from -24 to 24 for both the X and Y axes?
It’s important to define an “idle dead zone”, your joystick idle value range where you want your project to be in a resting position. If we don’t define this in the code, then components may not turn off reliably.
//== LEFT JOYSTICK DEADZONE ==//
if (ctl->axisY() > -25 && ctl->axisY() < 25 && ctl->axisX() > -25 && ctl->() < 25) {
// code for when left joystick is at idle
}
This long line of code carves out a dead zone for both the X and Y axes of the left joystick. Then, within the curly brackets, you can add your own code for turning certain components off.
Here, we’re basically saying: “if the Y value is greater than -25 AND (&&) it’s also less than 25 AND if the X value is greater than -25 AND it’s also less than 25, then do the thing within the curly brackets.
XY Example with the Right Joystick
The code blocks for the left joystick are a great template for controlling DC motors where perhaps you want to move a vehicle forward when thumbing up, move it in reverse when thumbing down and turn it left and right.
I wanted to give you another code option using the right joystick:

There are only two code blocks, one for joystick movement along the X axis and another for the Y axis.
This format can be used to control servos where moving the right joystick along the X axis moves one servo and then moving along the Y axis controls a different servo. Using a joystick like this allows you to coordinate the movement between two servos to accomplish a task, like operating a robotic arm.
Controlling Components with an ESP32 and Game Controller
Now that you know how to pair your game controller with an ESP32 using the Bluepad32 library and how button and joystick values work, it’s time to start adding code for the components you plan to use in your project!
In the next ESP32 game controller tutorial, I’ll show you how to light LEDs, accelerate motors and operate servos using this Arduino starter sketch.
