Copy the example below and paste it into the Arduino IDE.
You must change the ssid and password in the example code to your WiFi SSID and password before uploading this to your board. This is only necessary if you plan to use the over-the-air (OTA) update feature, otherwise you can leave these alone.
You can also change the sleepSeconds value if you want your controller to go into deep sleep sooner or later than the default 30 seconds.
Once you've made these changes, upload the code to your ItsyBitsy ESP32.
// SPDX-FileCopyrightText: 2024 John Park for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*
* Feather ESP32 Bluetooth LE gamepad https://github.com/lemmingDev/ESP32-BLE-Gamepad
* Deep sleep with wake on START button press
* https://randomnerdtutorials.com/esp32-deep-sleep-arduino-ide-wake-up-sources/
* OTA WiFi uploads
* https://docs.espressif.com/projects/arduino-esp32/en/latest/ota_web_update.html
* Sketch > Compile binary, then http://esp32.local/?userid=admin&pwd=admin
* pick compiled .bin, upload.
*/
#include <Arduino.h>
#include <BleGamepad.h>
#include <Adafruit_NeoPixel.h>
#include <esp_wifi.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
bool web_ota = false;
int sleepSeconds = 30; // how long is it inactive before going to sleep
const char* host = "esp32";
const char* ssid = "xxxxxxx"; // your WiFi SSID here
const char* password = "xxxxxxxx"; // your WiFi password here
WebServer server(80);
/*
* Login page
*/
const char* loginIndex =
"<form name='loginForm'>"
"<table width='20%' bgcolor='A09F9F' align='center'>"
"<tr>"
"<td colspan=2>"
"<center><font size=4><b>ESP32 Login Page</b></font></center>"
"<br>"
"</td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td>Username:</td>"
"<td><input type='text' size=25 name='userid'><br></td>"
"</tr>"
"<br>"
"<br>"
"<tr>"
"<td>Password:</td>"
"<td><input type='Password' size=25 name='pwd'><br></td>"
"<br>"
"<br>"
"</tr>"
"<tr>"
"<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
"</tr>"
"</table>"
"</form>"
"<script>"
"function check(form)"
"{"
"if(form.userid.value=='admin' && form.pwd.value=='admin')"
"{"
"window.open('/serverIndex')"
"}"
"else"
"{"
" alert('Error Password or Username')/*displays error message*/"
"}"
"}"
"</script>";
/*
* Server Index Page
*/
const char* serverIndex =
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
" $.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>";
////////////////////////////////////// GAMEPAD
#define numOfButtons 12
// sleep wake button definition (also update line in setup(): 'esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0);')
#define BUTTON_PIN_BITMASK 0x10 // start button on RTC GPIO pin 4 which is 0x10 (2^4 in hex)
// RTC_DATA_ATTR int bootCount = 0;
BleGamepad bleGamepad("ItsyController", "Adafruit", 100); // name, manufacturer, batt level to start
byte previousButtonStates[numOfButtons];
byte currentButtonStates[numOfButtons];
// ItsyBitsy EPS32: 13, 12, 14, 33, 32, 7, 5, 27, 15, 20, 8, 22, 21, 19, 36, 37, 38, 4, 26, 25
// RTC IO: 13, 12, 14, 33, 32, 27, 15, 36, 37, 38, 4, 26, 25
// pins that act funny: 5, 37, 22
byte buttonPins[numOfButtons] = { 13, 12, 14, 33, 32, 7, 27, 15, 21, 19, 4, 26 }; // ItsyBitsy
byte physicalButtons[numOfButtons] = { 1, 2, 4, 5, 7, 8, 15, 16, 13, 14, 12, 11 }; // controller assignments
// b0, b1, b3, b4, b6, b7, b14, b15, b12, b13, b10, b11
// gampad: O/b0, X/b1, ^/b3, []]/b4, l_trig/b6, r_trig/b7, up/b14 , down/b15 , left/b12 , right/b13, select/b11, start/b10
int last_button_press = millis();
int sleepTime = (sleepSeconds * 1000);
Adafruit_NeoPixel pixel(1, 0, NEO_GRB + NEO_KHZ800); // Itsy on-board NeoPixel
void setup()
{
Serial.begin(115200);
delay(500);
//Print the wakeup reason for ESP32
// print_wakeup_reason();
esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0); //1 = High, 0 = Low
for (byte currentPinIndex = 0; currentPinIndex < numOfButtons; currentPinIndex++)
{
pinMode(buttonPins[currentPinIndex], INPUT_PULLUP);
previousButtonStates[currentPinIndex] = HIGH;
currentButtonStates[currentPinIndex] = HIGH;
}
bleGamepad.begin();
delay(100);
pixel.begin();
pixel.clear();
if (web_ota) {
// Connect to WiFi network
WiFi.begin(ssid, password);
Serial.println("");
// Wait for connection for 20 seconds, then move on
unsigned long startTime = millis(); // Get the current time
while (!(WiFi.status() == WL_CONNECTED) && ((millis() - startTime) < 2000)) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
/*use mdns for host name resolution*/
if (!MDNS.begin(host)) { //http://esp32.local
Serial.println("Error setting up MDNS responder!");
while (1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
/*return index page which is stored in serverIndex */
server.on("/", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", loginIndex);
});
server.on("/serverIndex", HTTP_GET, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/html", serverIndex);
});
/*handling uploading firmware file */
server.on("/update", HTTP_POST, []() {
server.sendHeader("Connection", "close");
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update: %s\n", upload.filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
/* flashing firmware to ESP*/
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
Update.printError(Serial);
}
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) { //true to set the size to the current progress
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
} else {
Update.printError(Serial);
}
}
});
server.begin();
}
else {
Serial.println("");
Serial.println("WiFi connection timed out, you may need to update SSID/password. Moving on now.");
}
}
}
void loop()
{
if (web_ota) {
server.handleClient();
delay(1);
}
if (bleGamepad.isConnected())
{
pixel.setPixelColor(0, 0x000033);
pixel.show();
for (byte currentIndex = 0; currentIndex < numOfButtons; currentIndex++)
{
currentButtonStates[currentIndex] = digitalRead(buttonPins[currentIndex]);
if (currentButtonStates[currentIndex] != previousButtonStates[currentIndex])
{
last_button_press = millis(); // update last_button_press for sleep timing
if (currentButtonStates[currentIndex] == LOW)
{
bleGamepad.press(physicalButtons[currentIndex]);
}
else
{
bleGamepad.release(physicalButtons[currentIndex]);
}
}
}
if (currentButtonStates != previousButtonStates)
{
for (byte currentIndex = 0; currentIndex < numOfButtons; currentIndex++)
{
previousButtonStates[currentIndex] = currentButtonStates[currentIndex];
}
bleGamepad.sendReport();
}
if (millis() - last_button_press > sleepTime) {
server.stop();
delay(300);
esp_wifi_stop();
delay(300);
esp_deep_sleep_start();
}
}
}
Gamepad
You'll use the BleGamepad library to create the gamepad object:
BleGamepad bleGamepad("ItsyController", "Adafruit", 100); // name, manufacturer, batt level to start
You can pick your own string for the name, so in this case "ItsyController" will show up on your computer or other device during pairing. The battery level feature is not used in this project.
Deep Sleep
Deep sleep mode saves on battery consumption by turning off the ESP32's processor, WiFi and Bluetooth radios, while keeping the Ultra Low Power co-processor (ULP) active, checking for the wake-up call.
Wake up sources can include a timer, cap touch pin, and external sources, a.k.a. buttons. We'll use the PlayStation's start button as our wake up source.
The way the code works is to keep track of when buttons are pressed and if none has been touched for thirty seconds (or whatever sleepTime you pick), the server and WiFi radio are stopped, and then esp_deep_sleep_start(); is called.
if (millis() - last_button_press > sleepTime) {
server.stop();
delay(300);
esp_wifi_stop();
delay(300);
esp_deep_sleep_start();
}
Wake Up
While in deep sleep, the ULP can keep an eye on any of the real time clock (RTC) GPIO pins. On the ItsyBitsy ESP32 this is any of the following pins: 13, 12, 14, 33, 32, 27, 15, 36, 37, 38, 4, 26, 25.
We'll use pin 4, which is the one the start button is connected to:
esp_sleep_enable_ext0_wakeup(GPIO_NUM_4,0);
The '0' indicates that wake up will happen when the indicated pin 4 goes low.
Web Update
If you want to update the code after you've closed up your controller, you can use the over-the-air (OTA) web update.
Point your browser at the ItsyBitsy's server at http://esp32.local and login with:
- username = admin
- password = admin
Follow the info in this guide on compiling and uploading your firmware.
Page last edited January 21, 2025
Text editor powered by tinymce.