🔍

wireless, battery powered BLE gamepad based on ESP32

quick prototype ESP32 wireless gamepad

If you like to build a hustle free wireless, rechargable, BLE controller prototype based on ESP32, it is very helpful to focus on established development boards, that provide proper LIPO recharging chipsets. There is a wide, affordable palette of devices available – over here, we use the ESP32 WROOM setup, you can find in this devboard for textile projects VELLEMANN or a more reduced one by AZ Delivery ESP Lolin.

Also for convenient reasons, i prefer to use LIPO lithium rechargable battery packs – they come with a variety of capacity. I can recomment 100-500 mAh for this ESP32 project – you can go lower when using ESP8266 for sure.

BASIC GAMEPAD TEST SETUP IN CPP

We can use the ESP32 with its integrated Bluetooth to simulate any generic gamepad. With this, we have a pretty standard input device for a wide range of digital environments like Browser, Unity or Touchdesigner without further plugins or extensions. This approach works best with the pretty stable Bluetooth library for ESP32 by lemming – include the lib to your project: https://github.com/lemmingDev/ESP32-BLE-Gamepad


// include all necessary libs
#include <Arduino.h>
#include <BleGamepad.h>

// initalize controller - et custom device name, manufacturer and initial battery level
BleGamepad bleGamepad("TURBOCONTROLLER v1", "TURBOFLIP STUDIOS", 99); 

void setup()
{
   Serial.println("try initalize gamepad");
   bleGamepad.begin();
   Serial.begin(9600);

   delay(200);
   Serial.println("gamepad initalized");

    
}

void loop()
{

    // only operate when connected to host
    
    if (bleGamepad.isConnected()){


        // ------------------------------------------
        // simple test setup for axis and buttons   
        // ------------------------------------------     
        // Set the duration of a sine wave to simulate axis movement
        const int waveDuration = 5000;
        const int sampleRate = 20; // Set the sample rate in milliseconds
        const int numSamples = waveDuration / sampleRate;
        const float frequency = 0.1; // Set the frequency of the sine wave

        // Calculate sine wave values for x and y axes
        float xValue = 0.5 * sin(2.0 * PI * frequency * millis() / 1000.0) + 0.5;
        float yValue = 0.5 * sin(2.0 * PI * frequency * millis() / 300.0) + 0.5;

        // overwrite when working with real analog data from custom input sticks
        //xValue = float(joy_x)*0.00024414062;
        //yValue = float(joy_y)*0.00024414062;

        // hardcoded calibration - when hardware sends uncalibrated data
        if(xValue > 0.49 && xValue < 0.51 ){xValue = 0.5;}
        if(yValue > 0.49 && yValue < 0.51 ){yValue = 0.5;}

        // Convert float values to int16_t (16-bit)
        int16_t xIntValue = static_cast<int16_t>(xValue * 32767);
        int16_t yIntValue = static_cast<int16_t>(yValue * 32767);

        // Set the axes values
        bleGamepad.setLeftThumb(xIntValue, yIntValue);


         // Calculate sine wave values for x and y axes
        float xValue2 = 0.5 * sin(2.0 * PI * frequency * millis() / 100.0) + 0.5;
        float yValue2 = 0.5 * sin(2.0 * PI * frequency * millis() / 400.0) + 0.5;

        // Convert float values to int16_t (16-bit)
        int16_t xIntValue2 = static_cast<int16_t>(xValue2 * 32767);
        int16_t yIntValue2 = static_cast<int16_t>(yValue2 * 32767);

         // Set the axes values
        bleGamepad.setRightThumb(xIntValue2, yIntValue2);
   
  
    }
    
    // set a delay to avoid possible data overflow
    delay(10);
}



advanced ESP3 controller


#include <Arduino.h>
#include <BleGamepad.h>

#include <trxy_btn.h>

#define ACTLEDPIN 22 // Pin button is attached to

int tick = 0;

bool interaction = false;
unsigned long lastInteractionTime = 0;

 BleGamepad bleGamepad("TURBOCONTROLLER v2", "TURBOFLIP STUDIOS", 99); // Set custom device name, manufacturer and initial battery level

aBTN mainbtn(34,false,false);
aBTN shiftbtn(32,false,false);

aBTN btn3(26,true,true);
aBTN btn4(25,true,true);

// -------------------------------------


void enterDeepSleep() {

     esp_sleep_enable_ext0_wakeup(GPIO_NUM_25, LOW); // Wake up when BUTTON_1_PIN goes low
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_26, LOW); // Wake up when BUTTON_2_PIN goes low
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_32, LOW); // Wake up when BUTTON_3_PIN goes low
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, LOW); // Wake up when BUTTON_4_PIN goes low
 

    esp_deep_sleep(300e6); // 300 seconds = 300e6 microseconds
}

// -------------------------------------

void setup()
{
   
    pinMode(ACTLEDPIN, OUTPUT);


    BleGamepadConfiguration bleGamepadConfig;
    bleGamepadConfig.setAutoReport(false);
    bleGamepad.begin(&bleGamepadConfig);
   

   delay(200);
 // Serial.println("initalize gampad");

    
}

void blink(){

                digitalWrite(ACTLEDPIN, HIGH);
                 delay(20);
                  digitalWrite(ACTLEDPIN, LOW);
                    delay(20);
                   digitalWrite(ACTLEDPIN, HIGH);
                 delay(20);
                  digitalWrite(ACTLEDPIN, LOW);

}

void loop()
{


    if (bleGamepad.isConnected()){

         // indicate connection to host
    digitalWrite(ACTLEDPIN, LOW);

    btn3.operateBUTTON();
    btn4.operateBUTTON();
    mainbtn.operateBUTTON();
    shiftbtn.operateBUTTON();

// --------------------------------------------

        // Track interaction
        if (btn3.on_released || btn4.on_released || mainbtn.on_released || shiftbtn.on_released) {
           
            lastInteractionTime = millis();
        }

        // Check for inactivity
        unsigned long currentTime = millis();
        if (currentTime - lastInteractionTime >= 300000) { // 300 seconds = 300,000 milliseconds
            // If no interaction for 300 seconds, enter deep sleep
            enterDeepSleep();
        }



// ------------------------------------------

        if (btn4.on_released)
    {
                 digitalWrite(ACTLEDPIN,LOW);
               bleGamepad.release(BUTTON_4);
                interaction = false;
                 bleGamepad.sendReport();
                  blink();
    }

      if (btn4.on_pressed)
    {
                digitalWrite(ACTLEDPIN, HIGH );
               bleGamepad.press(BUTTON_4);
                interaction = true;
                 bleGamepad.sendReport();
    }




// ------------------------------------------

    if (btn3.on_released)
    {
        bleGamepad.release(BUTTON_3);
         bleGamepad.sendReport();
        delay(10);
    }

      if (btn3.on_pressed)
    {
        bleGamepad.press(BUTTON_3);
         bleGamepad.sendReport();
        delay(10);
         blink();
    }

// ------------------------------------------

    if (shiftbtn.on_released)
            {
            bleGamepad.release(BUTTON_2);
             bleGamepad.sendReport();
             digitalWrite(ACTLEDPIN,LOW);
            delay(10);

               
           
            }


    if (shiftbtn.on_pressed)
            {
                bleGamepad.press(BUTTON_2);
                 bleGamepad.sendReport();
               
                  digitalWrite(ACTLEDPIN,HIGH);
                delay(10);
               

            }

// ------------------------------------------


 if (mainbtn.on_released)
            {
                // Serial.println("mainbtn OFF");
           
                bleGamepad.release(BUTTON_1);
                 bleGamepad.sendReport();
                   digitalWrite(ACTLEDPIN,LOW);
                 interaction = false;
               delay(10);
            }

     if (mainbtn.on_pressed)
            {
              
             // Serial.println("mainbtn");
              
               bleGamepad.press(BUTTON_1);
                bleGamepad.sendReport();
                 digitalWrite(ACTLEDPIN, HIGH);
                interaction = true;
               delay(10);
            }else{
                
            }

        



        tick++;
        if(tick > 60){
            tick = 0;
            if( !interaction){

                 digitalWrite(ACTLEDPIN, HIGH);
                 delay(6);
                  digitalWrite(ACTLEDPIN, LOW);

            }
            
                   
        }


    }
    
   delay(10);

   return;
   // 
   // {
        
       

       // joy_x =  analogRead(JOYSTICK_X_PIN)    ;
       // joy_y = analogRead(JOYSTICK_Y_PIN) ;
        
        
        
       // Serial.println(joy_x);
        
         

       
       



/*
        // Set the duration of the sine wave in milliseconds
        const int waveDuration = 5000;
        const int sampleRate = 20; // Set the sample rate in milliseconds
        const int numSamples = waveDuration / sampleRate;
        const float frequency = 0.1; // Set the frequency of the sine wave

        // Calculate sine wave values for x and y axes
        float xValue = 0.5 * sin(2.0 * PI * frequency * millis() / 1000.0) + 0.5;
        float yValue = 0.5 * sin(2.0 * PI * frequency * millis() / 300.0) + 0.5;

        xValue = float(joy_x)*0.00024414062;
        yValue = float(joy_y)*0.00024414062;

        // hardcoded calib
        if(xValue > 0.49 && xValue < 0.51 ){xValue = 0.5;}
        if(yValue > 0.49 && yValue < 0.51 ){yValue = 0.5;}

        // Convert float values to int16_t (16-bit)
        int16_t xIntValue = static_cast<int16_t>(xValue * 32767);
        int16_t yIntValue = static_cast<int16_t>(yValue * 32767);

        // Set the axes values
        bleGamepad.setLeftThumb(xIntValue, yIntValue);


         // Calculate sine wave values for x and y axes
        float xValue2 = 0.5 * sin(2.0 * PI * frequency * millis() / 100.0) + 0.5;
        float yValue2 = 0.5 * sin(2.0 * PI * frequency * millis() / 400.0) + 0.5;

        // Convert float values to int16_t (16-bit)
        int16_t xIntValue2 = static_cast<int16_t>(xValue2 * 32767);
        int16_t yIntValue2 = static_cast<int16_t>(yValue2 * 32767);

         // Set the axes values
        bleGamepad.setRightThumb(xIntValue2, yIntValue2);
   
  
    }
    else
    {
       digitalWrite(LEDPIN, LOW);
    }
     */

    delay(10);
}



Simply put this helper class in your project to make button interaction more comfortable.

// -------------------------------
// TXY'S button helper class
// --------------------------------
// serves you wonderful functionality right on the plate :)
  
    // is_pressed = momentary state of button
    // is_holded = momentary state if button is down for at least 1s
      
    // on_pressed = triggered once if button is down
    // on_pressed = triggered once if button is up
    // on_holded = triggered once if button is down for at least 1s
  
  
class aBTN {
  private:
    int bpin;
    long ts = 0;
    bool previous_state = false;
    bool prev_is_holded = false;
    bool flipped_phase = false;
  
  public:
    bool is_pressed = false;
    bool is_holded = false;
  
    bool on_pressed = false;
    bool on_released = false;
    bool on_holded = false;

    bool need_pullup = false;
  
    aBTN(int bpin, bool _flipped_phase, bool _pu) {
      this->bpin = bpin;
      this->flipped_phase = _flipped_phase;
      this->need_pullup = _pu;
      init();
    }
    void init() {

      if(!need_pullup){
        pinMode(bpin, INPUT);
      }else{
        pinMode(bpin, INPUT_PULLUP);
      }
    }
  
  
    void operateBUTTON() {
  
      this->is_pressed = digitalRead(bpin);
  
        // flip phase if button phase is flipped
       if(this->flipped_phase){this->is_pressed = !this->is_pressed;}
  
        // reset press states
      this->on_pressed = false;
      this->on_released = false;
  
      // if there is any change of state :)
      if (this->previous_state != this->is_pressed) {
  
  
            if (this->is_pressed) {
              
            //button is pressed down -------
            this->ts = millis(); // set timestamp
            this->on_pressed = true;
  
            } else {
            //button is released -------
            this->on_released = true;
            this->is_holded = false;
            this->on_holded = false;
            this->prev_is_holded = false;
            }
  
            // buffer prev state to avoid repeat
            this->previous_state = this->is_pressed;
  
      }
  
        // reset on hold state first
        this->on_holded = false;
  
        // if button is holded for more than 1s and is still pressed!
      if (this->ts + 1000 < millis() && this->is_pressed ) {
  
        this->is_holded = true; // set is holded state each frame here
  
        if (this->prev_is_holded != this->is_holded && this->is_pressed ) {
  
          this->on_holded = true;
          this->prev_is_holded = this->is_holded;
            
        }
      }
    }
};

quick testing

You can evaluate your gamepad functions with this simple browser based tool immediately: https://hardwaretester.com/gamepad – This can give you a clue if all data is send in the right format and boundaries.

test your gamepad functions with this simple browser based tool