Hello Again!
I've been hard at work posting daily TikToks showing off the different capabilities of openmote, one of the best features that I personally love is as a media controller.
The ability to pull out your phone and skip, pause, even change the volume all with a wii-remote is the funniest and most head turning thing I can think of. The applications and devices that it works with is kinda shocking.
- In the car, even with carplay you can pause, skip
- a smartphone
- wearing headphones
- a computer
Check out my Tiktoks @Bird.Builds and see what I'm building, I've attached the Bluetooth media controller code for anyone interested.
Learn more about this project and subscribe for updates when the campaign goes live: openmote.io
/*
* OpenMote BLE Media Controller
*
* Turn your OpenMote into a Bluetooth media remote control!
* Control music playback and volume on any Bluetooth device.
*
* Controls:
* - Plus Button → Volume Up
* - Minus Button → Volume Down
* - D-Pad Left → Previous Track
* - D-Pad Right → Next Track
* - A Button → Play/Pause Toggle
*
* Features:
* - BLE HID Consumer Control (works with phones, tablets, computers)
* - LED feedback for button presses
* - Haptic feedback on actions
* - Connection status indicators
*/
#include <Arduino.h>
#include <OpenMote.h>
#include <NimBLEDevice.h>
#include <NimBLEHIDDevice.h>
// ===== DEBUG CONFIGURATION =====
#define DEBUG_SERIAL true
// ===== MEDIA CONTROL KEY CODES (Consumer Page) =====
#define MEDIA_PLAY_PAUSE 0xCD
#define MEDIA_NEXT_TRACK 0xB5
#define MEDIA_PREV_TRACK 0xB6
#define MEDIA_VOLUME_UP 0xE9
#define MEDIA_VOLUME_DOWN 0xEA
#define MEDIA_MUTE 0xE2
// ===== DEBOUNCE SETTINGS =====
#define DEBOUNCE_DELAY_MS 50
#define VOLUME_REPEAT_DELAY 200 // Faster repeat for volume buttons
// ===== GLOBAL OBJECTS =====
OpenMote mote;
// BLE HID Device
NimBLEHIDDevice* hid;
NimBLECharacteristic* input;
// ===== STATE VARIABLES =====
bool isConnected = false;
// Button state tracking
bool plusButtonPressed = false;
bool minusButtonPressed = false;
bool dpadLeftPressed = false;
bool dpadRightPressed = false;
bool aButtonPressed = false;
unsigned long lastDebounceTime = 0;
unsigned long lastVolumeRepeatTime = 0;
// ===== HID REPORT DESCRIPTOR - CONSUMER CONTROL ONLY =====
// This descriptor defines a pure media controller (no keyboard functionality)
const uint8_t hidReportDescriptor[] = {
0x05, 0x0C, // Usage Page (Consumer Devices)
0x09, 0x01, // Usage (Consumer Control)
0xA1, 0x01, // Collection (Application)
0x85, 0x01, // Report ID (1)
0x75, 0x10, // Report Size (16 bits)
0x95, 0x01, // Report Count (1)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x07, // Logical Maximum (2047)
0x19, 0x00, // Usage Minimum (0)
0x2A, 0xFF, 0x07, // Usage Maximum (2047)
0x81, 0x00, // Input (Data, Array, Absolute)
0xC0 // End Collection
};
// ===== CONSUMER CONTROL REPORT STRUCTURE =====
typedef struct {
uint16_t usage; // 16-bit consumer control usage code
} ConsumerReport;
ConsumerReport consumerReport = {0};
// ===== BLE SERVER CALLBACKS =====
class ServerCallbacks: public NimBLEServerCallbacks {
void onConnect(NimBLEServer* pServer) {
isConnected = true;
#if DEBUG_SERIAL
Serial.println(">>> BLE Device Connected!");
#endif
// Visual feedback: Quick LED flash
mote.turnOnAllLEDs();
delay(100);
mote.turnOffAllLEDs();
mote.rumblePulse(100);
}
void onDisconnect(NimBLEServer* pServer) {
isConnected = false;
#if DEBUG_SERIAL
Serial.println(">>> BLE Device Disconnected!");
#endif
// Restart advertising
NimBLEDevice::startAdvertising();
// Visual feedback: Slow blink
mote.blinkAllLEDs(300);
}
};
// ===== MEDIA CONTROL FUNCTIONS =====
// Send a consumer control command (press and release)
void sendMediaKey(uint16_t keyCode) {
if (!isConnected) return;
// Press key
consumerReport.usage = keyCode;
input->setValue((uint8_t*)&consumerReport, sizeof(consumerReport));
input->notify();
delay(50);
// Release key
consumerReport.usage = 0;
input->setValue((uint8_t*)&consumerReport, sizeof(consumerReport));
input->notify();
}
// ===== FEEDBACK FUNCTIONS =====
// Visual feedback for different button types
void showButtonFeedback(int buttonType) {
switch(buttonType) {
case 1: // Play/Pause - All LEDs pulse
mote.turnOnAllLEDs();
delay(80);
mote.turnOffAllLEDs();
break;
case 2: // Volume Up - LED1 & LED2
mote.turnOnLED1();
mote.turnOnLED2();
delay(80);
mote.turnOffLED1();
mote.turnOffLED2();
break;
case 3: // Volume Down - LED3 & LED4
mote.turnOnLED3();
mote.turnOnLED4();
delay(80);
mote.turnOffLED3();
mote.turnOffLED4();
break;
case 4: // Next Track - Right LEDs (2 & 4)
mote.turnOnLED2();
mote.turnOnLED4();
delay(80);
mote.turnOffLED2();
mote.turnOffLED4();
break;
case 5: // Previous Track - Left LEDs (1 & 3)
mote.turnOnLED1();
mote.turnOnLED3();
delay(80);
mote.turnOffLED1();
mote.turnOffLED3();
break;
}
}
// ===== SETUP =====
void setup() {
#if DEBUG_SERIAL
Serial.begin(115200);
delay(1000);
Serial.println("==========================================");
Serial.println("OpenMote BLE Media Controller");
Serial.println("==========================================");
Serial.println("Plus/Minus: Volume Up/Down");
Serial.println("D-Pad L/R: Previous/Next Track");
Serial.println("A Button: Play/Pause");
Serial.println("==========================================");
#endif
// Initialize OpenMote (no IMU needed for this project)
mote.begin();
#if DEBUG_SERIAL
Serial.println("✓ OpenMote initialized");
#endif
// Initialize NimBLE
NimBLEDevice::init("OpenMote Media Remote");
#if DEBUG_SERIAL
Serial.println("✓ BLE initialized");
#endif
// 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(0x0180); // Generic Remote Control appearance
pAdvertising->addServiceUUID(hid->hidService()->getUUID());
pAdvertising->start();
#if DEBUG_SERIAL
Serial.println("✓ BLE HID Media Controller started!");
Serial.println("Connect via Bluetooth to 'OpenMote Media Remote'");
Serial.println("==========================================");
Serial.println("");
#endif
// Startup sequence - cascade LEDs
mote.turnOnLED1();
delay(100);
mote.turnOnLED2();
delay(100);
mote.turnOnLED3();
delay(100);
mote.turnOnLED4();
delay(100);
mote.turnOffAllLEDs();
mote.rumblePulse(100);
}
// ===== MAIN LOOP =====
void loop() {
unsigned long currentTime = millis();
// Check BLE connection status
if (isConnected) {
// ===== PLUS BUTTON - VOLUME UP =====
bool plusState = mote.isPlusButtonPressed();
if (plusState) {
// Allow repeat for volume (hold button = continuous volume change)
if (!plusButtonPressed || (currentTime - lastVolumeRepeatTime >= VOLUME_REPEAT_DELAY)) {
if (currentTime - lastDebounceTime >= DEBOUNCE_DELAY_MS) {
#if DEBUG_SERIAL
Serial.println(">>> Volume Up");
#endif
sendMediaKey(MEDIA_VOLUME_UP);
showButtonFeedback(2);
mote.rumblePulse(30);
plusButtonPressed = true;
lastDebounceTime = currentTime;
lastVolumeRepeatTime = currentTime;
}
}
} else {
plusButtonPressed = false;
}
// ===== MINUS BUTTON - VOLUME DOWN =====
bool minusState = mote.isMinusButtonPressed();
if (minusState) {
// Allow repeat for volume (hold button = continuous volume change)
if (!minusButtonPressed || (currentTime - lastVolumeRepeatTime >= VOLUME_REPEAT_DELAY)) {
if (currentTime - lastDebounceTime >= DEBOUNCE_DELAY_MS) {
#if DEBUG_SERIAL
Serial.println(">>> Volume Down");
#endif
sendMediaKey(MEDIA_VOLUME_DOWN);
showButtonFeedback(3);
mote.rumblePulse(30);
minusButtonPressed = true;
lastDebounceTime = currentTime;
lastVolumeRepeatTime = currentTime;
}
}
} else {
minusButtonPressed = false;
}
// ===== D-PAD RIGHT - NEXT TRACK =====
bool dpadRightState = mote.isDPadRightPressed();
if (dpadRightState && !dpadRightPressed) {
if (currentTime - lastDebounceTime >= DEBOUNCE_DELAY_MS) {
#if DEBUG_SERIAL
Serial.println(">>> Next Track");
#endif
sendMediaKey(MEDIA_NEXT_TRACK);
showButtonFeedback(4);
mote.rumblePulse(80);
dpadRightPressed = true;
lastDebounceTime = currentTime;
}
} else if (!dpadRightState) {
dpadRightPressed = false;
}
// ===== D-PAD LEFT - PREVIOUS TRACK =====
bool dpadLeftState = mote.isDPadLeftPressed();
if (dpadLeftState && !dpadLeftPressed) {
if (currentTime - lastDebounceTime >= DEBOUNCE_DELAY_MS) {
#if DEBUG_SERIAL
Serial.println(">>> Previous Track");
#endif
sendMediaKey(MEDIA_PREV_TRACK);
showButtonFeedback(5);
mote.rumblePulse(80);
dpadLeftPressed = true;
lastDebounceTime = currentTime;
}
} else if (!dpadLeftState) {
dpadLeftPressed = false;
}
// ===== A BUTTON - PLAY/PAUSE =====
bool aState = mote.isAButtonPressed();
if (aState && !aButtonPressed) {
if (currentTime - lastDebounceTime >= DEBOUNCE_DELAY_MS) {
#if DEBUG_SERIAL
Serial.println(">>> Play/Pause");
#endif
sendMediaKey(MEDIA_PLAY_PAUSE);
showButtonFeedback(1);
mote.rumblePulse(100);
aButtonPressed = true;
lastDebounceTime = currentTime;
}
} else if (!aState) {
aButtonPressed = false;
}
} else {
// Not connected - reset button states
plusButtonPressed = false;
minusButtonPressed = false;
dpadLeftPressed = false;
dpadRightPressed = false;
aButtonPressed = false;
// Show BLE disconnected status with LED2 slow blink
static unsigned long lastBlinkTime = 0;
static bool blinkState = false;
if (currentTime - lastBlinkTime >= 1000) {
blinkState = !blinkState;
if (blinkState) {
mote.turnOnLED2();
} else {
mote.turnOffLED2();
}
lastBlinkTime = currentTime;
}
}
delay(10);
}
Gangwa Labs
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.