Close

BLE keyboard for computer gaming

A project log for OpenMote: Arduino-Compatible Controller for Makers

Transform Your Old Wii Remotes into a Versatile Tool for Home Automation, Gaming, and DIY Projects

gangwa-labsGangwa Labs 10/13/2025 at 19:360 Comments

Greetings! As promised today I'm going to be talking about turning OpenMote into a BLE keyboard for computer control!

For some low level gaming and simple keystroke replacement this project log is all you need to know! Today I configured OpenMote to play the Dino game and pacman on my computer wirelessly! 

I'm hoping to get some more complex and complicated gaming controller connections -- think dolphin -- in the next couple of weeks!

If you're looking for a fun and silly way to connect and control your computer with a wii-remote then look no further than the code I provide today. I tried getting it to work with a couple other BLE libraries and found the NimBLE was easily the best one as it worked first time out of the box.

Enjoy and stay fun!

#include <Arduino.h>
#include <NimBLEDevice.h>
#include <NimBLEHIDDevice.h>

// Pin definitions based on your custom board
#define A_BUTT 14
#define UP_BUTT 11
#define DOWN_BUTT 12
#define LEFT_BUTT 4
#define RIGHT_BUTT 15

// BLE HID Keyboard
NimBLEHIDDevice* hid;
NimBLECharacteristic* input;
NimBLECharacteristic* output;

// Button state variables
bool aButtonPressed = false;
bool upButtonPressed = false;
bool downButtonPressed = false;
bool leftButtonPressed = false;
bool rightButtonPressed = false;

unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

// Status tracking
unsigned long lastStatusTime = 0;
const unsigned long statusInterval = 5000;
bool isConnected = false;

// HID Report Descriptor for Keyboard
const uint8_t hidReportDescriptor[] = {
  0x05, 0x01,        // Usage Page (Generic Desktop)
  0x09, 0x06,        // Usage (Keyboard)
  0xA1, 0x01,        // Collection (Application)
  0x85, 0x01,        //   Report ID (1)
  0x05, 0x07,        //   Usage Page (Key Codes)
  0x19, 0xE0,        //   Usage Minimum (224)
  0x29, 0xE7,        //   Usage Maximum (231)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x01,        //   Logical Maximum (1)
  0x75, 0x01,        //   Report Size (1)
  0x95, 0x08,        //   Report Count (8)
  0x81, 0x02,        //   Input (Data, Variable, Absolute)
  0x95, 0x01,        //   Report Count (1)
  0x75, 0x08,        //   Report Size (8)
  0x81, 0x01,        //   Input (Constant)
  0x95, 0x06,        //   Report Count (6)
  0x75, 0x08,        //   Report Size (8)
  0x15, 0x00,        //   Logical Minimum (0)
  0x25, 0x65,        //   Logical Maximum (101)
  0x05, 0x07,        //   Usage Page (Key Codes)
  0x19, 0x00,        //   Usage Minimum (0)
  0x29, 0x65,        //   Usage Maximum (101)
  0x81, 0x00,        //   Input (Data, Array)
  0xC0               // End Collection
};

// Keyboard report structure
typedef struct {
  uint8_t modifiers;
  uint8_t reserved;
  uint8_t keys[6];
} KeyReport;

KeyReport keyReport = {0};

// BLE Server Callbacks
class ServerCallbacks: public NimBLEServerCallbacks {
  void onConnect(NimBLEServer* pServer) {
    isConnected = true;
    Serial.println(">>> BLE Client Connected!");
  }

  void onDisconnect(NimBLEServer* pServer) {
    isConnected = false;
    Serial.println(">>> BLE Client Disconnected!");
    NimBLEDevice::startAdvertising();
  }
};

void sendKey(uint8_t key) {
  keyReport.keys[0] = key;
  input->setValue((uint8_t*)&keyReport, sizeof(keyReport));
  input->notify();

  delay(50);

  // Release
  keyReport.keys[0] = 0;
  input->setValue((uint8_t*)&keyReport, sizeof(keyReport));
  input->notify();
}

void setup() {
  // Initialize serial for debugging
  Serial.begin(115200);
  delay(1000);

  Serial.println("=================================");
  Serial.println("OpenMote NimBLE Keyboard Starting...");
  Serial.println("=================================");

  // Configure button pins
  pinMode(A_BUTT, INPUT_PULLUP);
  pinMode(UP_BUTT, INPUT_PULLUP);
  pinMode(DOWN_BUTT, INPUT_PULLUP);
  pinMode(LEFT_BUTT, INPUT_PULLUP);
  pinMode(RIGHT_BUTT, INPUT_PULLUP);

  // Initialize NimBLE
  NimBLEDevice::init("OpenMote Controller");

  // Create BLE Server
  NimBLEServer *pServer = NimBLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  // Create HID Device
  hid = new NimBLEHIDDevice(pServer);

  // Set HID parameters
  hid->manufacturer()->setValue("OpenMote");
  hid->pnp(0x02, 0xe502, 0xa111, 0x0210);
  hid->hidInfo(0x00, 0x01);

  // Set Report Map
  hid->reportMap((uint8_t*)hidReportDescriptor, sizeof(hidReportDescriptor));

  // Create input report characteristic
  input = hid->inputReport(1);

  // Start HID service
  hid->startServices();

  // Start advertising
  NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
  pAdvertising->setAppearance(0x03C1); // Keyboard appearance
  pAdvertising->addServiceUUID(hid->hidService()->getUUID());
  pAdvertising->start();

  Serial.println("BLE HID Keyboard started!");
  Serial.println("Connect via Bluetooth to 'OpenMote Controller'");
  Serial.println("=================================");
}

void loop() {
  unsigned long currentTime = millis();

  // Periodic status message
  if (currentTime - lastStatusTime > statusInterval) {
    lastStatusTime = currentTime;
    Serial.print("Status: ");
    Serial.print(isConnected ? "BLE Connected ✓" : "BLE Disconnected ✗");
    Serial.print(" | A Button: ");
    Serial.println(digitalRead(A_BUTT) == LOW ? "PRESSED" : "Released");
  }

  // Check if BLE keyboard is connected
  if(isConnected) {

    // A Button - Spacebar
    bool aState = (digitalRead(A_BUTT) == LOW);
    if (aState && !aButtonPressed) {
      if ((currentTime - lastDebounceTime) > debounceDelay) {
        aButtonPressed = true;
        lastDebounceTime = currentTime;
        Serial.println(">>> A Button - Spacebar");
        sendKey(0x2C); // Spacebar
      }
    } else if (!aState && aButtonPressed) {
      aButtonPressed = false;
    }

    // UP Button - Up Arrow
    bool upState = (digitalRead(UP_BUTT) == LOW);
    if (upState && !upButtonPressed) {
      if ((currentTime - lastDebounceTime) > debounceDelay) {
        upButtonPressed = true;
        lastDebounceTime = currentTime;
        Serial.println(">>> UP Button - Up Arrow");
        sendKey(0x52); // Up Arrow
      }
    } else if (!upState && upButtonPressed) {
      upButtonPressed = false;
    }

    // DOWN Button - Down Arrow
    bool downState = (digitalRead(DOWN_BUTT) == LOW);
    if (downState && !downButtonPressed) {
      if ((currentTime - lastDebounceTime) > debounceDelay) {
        downButtonPressed = true;
        lastDebounceTime = currentTime;
        Serial.println(">>> DOWN Button - Down Arrow");
        sendKey(0x51); // Down Arrow
      }
    } else if (!downState && downButtonPressed) {
      downButtonPressed = false;
    }

    // LEFT Button - Left Arrow
    bool leftState = (digitalRead(LEFT_BUTT) == LOW);
    if (leftState && !leftButtonPressed) {
      if ((currentTime - lastDebounceTime) > debounceDelay) {
        leftButtonPressed = true;
        lastDebounceTime = currentTime;
        Serial.println(">>> LEFT Button - Left Arrow");
        sendKey(0x50); // Left Arrow
      }
    } else if (!leftState && leftButtonPressed) {
      leftButtonPressed = false;
    }

    // RIGHT Button - Right Arrow
    bool rightState = (digitalRead(RIGHT_BUTT) == LOW);
    if (rightState && !rightButtonPressed) {
      if ((currentTime - lastDebounceTime) > debounceDelay) {
        rightButtonPressed = true;
        lastDebounceTime = currentTime;
        Serial.println(">>> RIGHT Button - Right Arrow");
        sendKey(0x4F); // Right Arrow
      }
    } else if (!rightState && rightButtonPressed) {
      rightButtonPressed = false;
    }

  } else {
    // Reset button states when disconnected
    aButtonPressed = false;
    upButtonPressed = false;
    downButtonPressed = false;
    leftButtonPressed = false;
    rightButtonPressed = false;
  }

  // Small delay to prevent overwhelming the processor
  delay(10);
}

Discussions