ESP32 encoder and ble-gamepad

Hi, im building a gamepad, im using an ESP32 connected via Bluetooth.

Wiring is simple, the pins are connected to vcc with 10k resistors, and to 22,23 pins, common to GND

This is the code I have:

#include <ESP32Encoder.h>     // https://github.com/madhephaestus/ESP32Encoder/
#include <BleGamepad.h>       // https://github.com/MagnusThome/ESP32-BLE-Gamepad

BleGamepad bleGamepad("Wheel", "Arduino", 100);

#define MAXENC 1
uint8_t uppPin[MAXENC] = {22};
uint8_t dwnPin[MAXENC] = {23};
uint8_t encoderUpp[MAXENC] = {4};
uint8_t encoderDwn[MAXENC] = {5};
ESP32Encoder encoder[MAXENC];
unsigned long holdoff[MAXENC] = {0};
int32_t prevenccntr[MAXENC] = {1};
bool prevprs[MAXENC] = {0};
#define HOLDOFFTIME 150   // TO PREVENT MULTIPLE ROTATE "CLICKS" WITH CHEAP ENCODERS WHEN ONLY ONE CLICK IS INTENDED

void setup() {
  Serial.begin(115200);

  for (uint8_t i = 0; i < MAXENC; i++) {
    encoder[i].clearCount();
    encoder[i].attachSingleEdge(dwnPin[i], uppPin[i]);
  }
  //customKeypad.setHoldTime(1);
  bleGamepad.begin();
  Serial.println("Booted!");
}

void loop() {

  unsigned long now = millis();


  // -- ROTARY ENCODERS : ROTATION -- //

  for (uint8_t i = 0; i < MAXENC; i++) {
    int32_t cntr = encoder[i].getCount();
    if (cntr != prevenccntr[i]) {
      if (!holdoff[i]) {
        if (cntr > prevenccntr[i]) {
          sendKey(encoderUpp[i]);
        }
        if (cntr < prevenccntr[i]) {
          sendKey(encoderDwn[i]);
        }
        holdoff[i] = now;
        if (holdoff[i] == 0) holdoff[i] = 1; // SAFEGUARD WRAP AROUND OF millis() (WHICH IS TO 0) SINCE holdoff[i]==0 HAS A SPECIAL MEANING ABOVE
      }
      else if (now - holdoff[i] > HOLDOFFTIME) {
        prevenccntr[i] = encoder[i].getCount();
        holdoff[i] = 0;
      }
    }


  }
}

void sendKey(uint8_t key) {
  uint32_t gamepadbutton = key;
  Serial.print("pulse\t");
  Serial.println(key);
  if (bleGamepad.isConnected()) {
    bleGamepad.press(gamepadbutton);
    delay(150);
    bleGamepad.release(gamepadbutton);
  }
}

The problem with that code is that jstest-gtk shows, when rotating CW, a good input, but, when clicking CCW, every.... 20 clicks, it sends the CW input.

Is a debounce problem? a broken encoder?

Also I tried this to use interrupts (I prefer use them ESP32 has a lot of interrupt pins, and I think its better that keep scanning a matrix or array of encoders)

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

#ifndef ESP32
#error ESP32 Only
#endif

void handleEncoder(void *pvParameters);
void ESP_ISR callBack(NewEncoder *encPtr, const volatile NewEncoder::EncoderState *state, void *uPtr);
QueueHandle_t encoderQueue;
volatile int16_t prevEncoderValue;
volatile int16_t prevEncoderValue1;

#define numOfButtons 16
#define numOfHatSwitches 2
byte previousButtonStates[numOfButtons];
byte currentButtonStates[numOfButtons];

BleGamepad bleGamepad;
BleGamepadConfiguration bleGamepadConfig;
uint8_t newMACAddress[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF - 0x02};

void setup() {
  Serial.begin(115200);
  delay(1000);

  BaseType_t success = xTaskCreatePinnedToCore(handleEncoder, "Handle Encoder", 1900, NULL, 2, NULL, 0);
  if (!success) {
    printf("Failed to create handleEncoder task. Aborting.\n");
    while (1) {
      yield();
    }
  }

  bleGamepadConfig.setAutoReport(false);
  bleGamepadConfig.setControllerType(CONTROLLER_TYPE_GAMEPAD); // CONTROLLER_TYPE_JOYSTICK, CONTROLLER_TYPE_GAMEPAD (DEFAULT), CONTROLLER_TYPE_MULTI_AXIS
  bleGamepadConfig.setButtonCount(numOfButtons);
  bleGamepadConfig.setHatSwitchCount(numOfHatSwitches);
  bleGamepadConfig.setVid(0xe502);
  bleGamepadConfig.setPid(0xabcd);
  bleGamepad.begin(&bleGamepadConfig);
  esp_base_mac_addr_set(&newMACAddress[0]);

}

void loop() {
}

void sendKey(uint8_t key) {
    uint32_t gamepadbutton = key;
    Serial.print("pulse\t");
    Serial.println(key);
    if(bleGamepad.isConnected()) {
      bleGamepad.press(gamepadbutton);
    }
}

void handleEncoder(void *pvParameters) {
  NewEncoder::EncoderState currentEncoderstate;
  NewEncoder::EncoderState currentEncoderstate1;
  int16_t currentValue;
  int16_t currentValue1;

  encoderQueue = xQueueCreate(1, sizeof(NewEncoder::EncoderState));
  if (encoderQueue == nullptr) {
    printf("Failed to create encoderQueue. Aborting\n");
    vTaskDelete(nullptr);
  }

  // This example uses Pins 25 & 26 for Encoder. Specify correct pins for your ESP32 / Encoder setup. See README for meaning of constructor arguments.
  // Use FULL_PULSE for encoders that produce one complete quadrature pulse per detnet, such as: https://www.adafruit.com/product/377
  // Use HALF_PULSE for endoders that produce one complete quadrature pulse for every two detents, such as: https://www.mouser.com/ProductDetail/alps/ec11e15244g1/?qs=YMSFtX0bdJDiV4LBO61anw==&countrycode=US&currencycode=USD
  NewEncoder *encoder1 = new NewEncoder(25, 26, -100, 100, 0, FULL_PULSE);
  NewEncoder *encoder2 = new NewEncoder(22, 23, -100, 100, 0, FULL_PULSE);

  if (encoder1 == nullptr) {
    printf("Failed to allocate NewEncoder object. Aborting.\n");
    vTaskDelete(nullptr);
  }

  if (!encoder1->begin()) {
    printf("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.\n");
    delete encoder1;
    vTaskDelete(nullptr);
  }

  encoder1->getState(currentEncoderstate);
  prevEncoderValue = currentEncoderstate.currentValue;
  printf("Encoder Successfully Started at value = %d\n", prevEncoderValue);
  encoder1->attachCallback(callBack);

  if (encoder2 == nullptr) {
    printf("Failed to allocate NewEncoder object. Aborting.\n");
    vTaskDelete(nullptr);
  }

  if (!encoder2->begin()) {
    printf("Encoder Failed to Start. Check pin assignments and available interrupts. Aborting.\n");
    delete encoder2;
    vTaskDelete(nullptr);
  }

  encoder2->getState(currentEncoderstate1);
  prevEncoderValue1 = currentEncoderstate1.currentValue;
  printf("Encoder Successfully Started at value = %d\n", prevEncoderValue1);
  encoder2->attachCallback(callBack);


  for (;;) {
    xQueueReceive(encoderQueue, &currentEncoderstate, portMAX_DELAY);
    printf("Encoder: ");
    currentValue = currentEncoderstate.currentValue;
    if (currentValue != prevEncoderValue) {
      switch (currentEncoderstate.currentClick) {
        case NewEncoder::UpClick:
          sendKey(4);
          break;

        case NewEncoder::DownClick:
         sendKey(5);
          break;

        default:
          break;
      }
      
      if (currentButtonStates != previousButtonStates)
      {
        for (byte currentIndex = 0; currentIndex < numOfButtons; currentIndex++)
        {
          previousButtonStates[currentIndex] = currentButtonStates[currentIndex];
        }

        bleGamepad.sendReport();
      }
      prevEncoderValue = currentValue;
    } else {

    }
  }
  vTaskDelete(nullptr);
}

void ESP_ISR callBack(NewEncoder*encPtr, const volatile NewEncoder::EncoderState *state, void *uPtr) {
  BaseType_t pxHigherPriorityTaskWoken = pdFALSE;

  xQueueOverwriteFromISR(encoderQueue, (void * )state, &pxHigherPriorityTaskWoken);
  if (pxHigherPriorityTaskWoken) {
    portYIELD_FROM_ISR();
  }
}

That second code, "works" as is... but, i need to release the key (it keeps pressed before the first rotation CW and CCW) so the logic editing is:

void sendKey(uint8_t key) {
    uint32_t gamepadbutton = key;
    Serial.print("pulse\t");
    Serial.println(key);
    if(bleGamepad.isConnected()) {
      bleGamepad.press(gamepadbutton);
      delay(150);
      bleGamepad.release(gamepadbutton);
    }
}

But it doesnt work, now, when rotating, nothing is clicked.

So, if I need to choose, i prefer to solve that second problem, i want to use interrupts... but if its not possible... well, if you know why a rotary messes clicks CCW but no CW and how to solve it... will be very thankful.

Thanks!

Please explain that more clearly.

1 Like

The interrupt, uses NewEncoder::UpClick: and NewEncoder::DownClick: functions.

they call sendkey with desired button.

sendkey "presses" that button with blegaming.press(button)

But it keeps it pressed, It needs to release them, with blegaming.release(button) after some delay.

Without that last step, CW and CCW pressed the correct gamepad button (and kept them pressed)

Adding a delay and the release call (as in my last piece of code) breaks all, rotating encoder never clicks.

I dont know why, maybe press/release from blegaming library cant work with interrupts, or my code its missing something (this options may be the correct)

thanks

It seems you have some basic misunderstandings about how your own code works:

UpClick and DownClick are not functions. They are enums defined in the NewEncoder scope.

No they don't. The sendKey() function is called by the handleEncoder task once it picks up a new encoder activity from the encoderQueue queue. That does not happen in interrupt context.

Do you see the results of these prints statement in your updated code?

    Serial.print("pulse\t");
    Serial.println(key);

Do you see them on every turn of the encoder, or only the first?

The first thing I would do add some more debug prints. Maybe that will show where things are hanging up.

void sendKey(uint8_t key) {
  uint32_t gamepadbutton = key;
  Serial.print("pulse\t");
  Serial.println(key);
  if (bleGamepad.isConnected()) {
    Serial.println("Calling bleGamepad.press()");
    bleGamepad.press(gamepadbutton);
    Serial.println("Returned from bleGamepad.press()");
    delay(150);
    Serial.println("Calling bleGamepad.release()");
    bleGamepad.release(gamepadbutton);
    Serial.println("Returned from bleGamepad.release()");
  }
}

Based on those results, I might try commenting out the actual calls to the bleGamepad functions:

void sendKey(uint8_t key) {
  uint32_t gamepadbutton = key;
  Serial.print("pulse\t");
  Serial.println(key);
  if (bleGamepad.isConnected()) {
    Serial.println("Calling bleGamepad.press()");
    //bleGamepad.press(gamepadbutton);
    Serial.println("Returned from bleGamepad.press()");
    delay(150);
    Serial.println("Calling bleGamepad.release()");
    //bleGamepad.release(gamepadbutton);
    Serial.println("Returned from bleGamepad.release()");
  }
}

Then do you see the debugs prints on every encoder click?

I have changed everything, now my encoder goes through a hardware filter (resistors and capacitors) an hex inverter, and a 7474 IC, then, I read in 7474 output a 0 or 1, and in CLK got the pulses, arduino reads pulses, and takes de output value.

image

And this is the code:

#include <BleGamepad.h>

const int clk = 22;
const int dt = 23;
volatile byte counter = 0;
#define numOfButtons 64
#define numOfHatSwitches 4
volatile int button = -1;
BleGamepad bleGamepad("Custom Contoller Name", "lemmingDev", 100); // Set custom device name, manufacturer and initial battery level
BleGamepadConfiguration bleGamepadConfig;                          // Create a BleGamepadConfiguration object to store all of the options
void attachInterruptTask(void *pvParameters);
void subirfc();

void setup() {
  Serial.begin(115200);
  xTaskCreatePinnedToCore(attachInterruptTask, "Attach Interrupt Task", 2000, NULL, 6, NULL, 0);
  delay(500);
  bleGamepadConfig.setAutoReport(false);
  bleGamepadConfig.setControllerType(CONTROLLER_TYPE_GAMEPAD); // CONTROLLER_TYPE_JOYSTICK, CONTROLLER_TYPE_GAMEPAD (DEFAULT), CONTROLLER_TYPE_MULTI_AXIS
  bleGamepadConfig.setButtonCount(numOfButtons);
  bleGamepadConfig.setHatSwitchCount(numOfHatSwitches);
  bleGamepadConfig.setVid(0xe502);
  bleGamepadConfig.setPid(0xabcd);
  // Some non-Windows operating systems and web based gamepad testers don't like min axis set below 0, so 0 is set by default
  bleGamepad.begin(&bleGamepadConfig); // Simulation controls, special buttons and hats 2/3/4 are disabled by default
}
void attachInterruptTask(void *pvParameters) {
  //Set BTN as input pull-up
  pinMode(clk, INPUT);
  pinMode(dt, INPUT);

  //Attach Interrupt in core 0
  attachInterrupt(digitalPinToInterrupt(clk), subirfc, CHANGE);
  while (true) {
    Serial.println("Task 1 Running");
    delay(500);
  }
  vTaskDelete(NULL);
}

void subirfc() {

  if (digitalRead(23) == LOW) {
    button = 5;
  } else {
    button = 4;
  }
}

void loop() {
  Serial.println(button);
  if (bleGamepad.isConnected() && button > 0)
  {
    bleGamepad.press(button);
    bleGamepad.sendReport();
    delay(150);
    bleGamepad.release(button);
    bleGamepad.sendReport();
    button = -1;
  }
}

It works perfectly.no library, and very little (or inexistent) bounce.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.