Project 2: Bluetooth Low Energy (BLE) Remote Control

Project 2: Bluetooth Low Energy (BLE) Remote Control

Project Overview

Attribute Value
Difficulty Intermediate
Time Estimate 2-3 weeks
Main Language C
Alternatives MicroPython, Rust, Arduino C++
Primary Book Getting Started with Bluetooth Low Energy by Kevin Townsend
Knowledge Areas Embedded Systems, BLE, GATT, FreeRTOS, Interrupts

What Youโ€™ll Build

A wireless controller that pairs with your phone via BLE, sends button presses and joystick positions, and can control games or applications on your computer/phone.

Physical Setup:

  • ESP32 with 4-8 tactile buttons and a dual-axis joystick
  • LED indicators for connection status and battery level
  • 3.7V LiPo battery for portable operation
  • Optional: 3D-printed enclosure

What Youโ€™ll See on Your Phone:

Bluetooth Settings (iOS/Android):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Bluetooth Devices              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                โ”‚
โ”‚ ESP32 GamePad     [Connected]  โ”‚
โ”‚ Battery: 87%                   โ”‚
โ”‚                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

BLE Scanner App (nRF Connect):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ESP32 GamePad                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Services:                      โ”‚
โ”‚                                โ”‚
โ”‚ Battery Service (0x180F)       โ”‚
โ”‚   Battery Level: 87%           โ”‚
โ”‚                                โ”‚
โ”‚ Custom Controller Service      โ”‚
โ”‚   Buttons (Read/Notify)        โ”‚
โ”‚   Value: 0x01 (A pressed)      โ”‚
โ”‚                                โ”‚
โ”‚   Joystick X (Read/Notify)     โ”‚
โ”‚   Value: 255 (right)           โ”‚
โ”‚                                โ”‚
โ”‚   Joystick Y (Read/Notify)     โ”‚
โ”‚   Value: 128 (centered)        โ”‚
โ”‚                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Learning Objectives

By completing this project, you will be able to:

  1. Implement a BLE GATT server with custom services and characteristics
  2. Handle hardware interrupts for responsive button input
  3. Implement button debouncing in software
  4. Read analog joystick values using ADC with calibration
  5. Manage BLE connection lifecycle (advertising, pairing, disconnect)
  6. Optimize power consumption for battery operation
  7. Use FreeRTOS tasks for concurrent button and BLE handling

Deep Theoretical Foundation

Bluetooth Low Energy vs Classic Bluetooth

BLE and Classic Bluetooth are fundamentally different protocols that happen to share the Bluetooth name and some radio frequencies.

Classic Bluetooth:                 BLE (Bluetooth Low Energy):

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Continuous stream   โ”‚           โ”‚ Intermittent bursts โ”‚
โ”‚ Audio, file transferโ”‚           โ”‚ Sensors, buttons    โ”‚
โ”‚ High bandwidth      โ”‚           โ”‚ Low bandwidth       โ”‚
โ”‚ Always connected    โ”‚           โ”‚ Sleep between eventsโ”‚
โ”‚ ~30mA active        โ”‚           โ”‚ ~5-15mA active      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Feature Classic Bluetooth BLE
Data Rate 1-3 Mbps 125 Kbps - 2 Mbps
Range ~10m ~10-100m
Latency 100+ ms 6-20 ms
Power 30-100 mA 5-15 mA
Use Case Audio, keyboards Sensors, beacons

Why BLE for a game controller? Lower latency than you might expect (can achieve <20ms), excellent battery life, and native support on all modern phones without pairing in settings.

The BLE Protocol Stack

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Application                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                         GATT                                 โ”‚
โ”‚            (Generic Attribute Profile)                       โ”‚
โ”‚         Services โ†’ Characteristics โ†’ Descriptors             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                          ATT                                 โ”‚
โ”‚              (Attribute Protocol)                            โ”‚
โ”‚              Read, Write, Notify                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                          GAP                                 โ”‚
โ”‚              (Generic Access Profile)                        โ”‚
โ”‚          Advertising, Discovery, Connection                  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                      L2CAP                                   โ”‚
โ”‚            (Logical Link Control)                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                    Link Layer                                โ”‚
โ”‚        Connection state machine, packets                     โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                   Physical Layer                             โ”‚
โ”‚              2.4 GHz radio, channels                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

For this project, youโ€™ll work primarily with GAP (advertising, connections) and GATT (services, characteristics).

GATT: The Heart of BLE Data

GATT (Generic Attribute Profile) defines how BLE devices expose data. Think of it as a structured database of values.

GATT Hierarchy

Server (ESP32)
โ””โ”€โ”€ Service: Controller (UUID: 0xFF01)
    โ”œโ”€โ”€ Characteristic: Buttons
    โ”‚   โ”œโ”€โ”€ Value: 0x01 (1 byte)
    โ”‚   โ”œโ”€โ”€ Properties: Read, Notify
    โ”‚   โ””โ”€โ”€ Descriptor: CCCD (enable notifications)
    โ”œโ”€โ”€ Characteristic: Joystick X
    โ”‚   โ”œโ”€โ”€ Value: 128 (1 byte, 0-255)
    โ”‚   โ””โ”€โ”€ Properties: Read, Notify
    โ””โ”€โ”€ Characteristic: Joystick Y
        โ”œโ”€โ”€ Value: 128 (1 byte, 0-255)
        โ””โ”€โ”€ Properties: Read, Notify
โ””โ”€โ”€ Service: Battery (UUID: 0x180F) [Standard]
    โ””โ”€โ”€ Characteristic: Battery Level
        โ”œโ”€โ”€ Value: 87 (1 byte, 0-100)
        โ””โ”€โ”€ Properties: Read, Notify

UUIDs: Identifying Services and Characteristics

UUID Type Format Example Use
16-bit 0xNNNN 0x180F Standard services (Battery, Heart Rate)
128-bit xxxxxxxx-โ€ฆ Custom Your own services

Standard 16-bit UUIDs are defined by the Bluetooth SIG. Using them makes your device compatible with standard clients. For example, the Battery Service (0x180F) automatically shows battery level in phone settings.

Custom UUIDs use the format: 0000XXXX-0000-1000-8000-00805F9B34FB where XXXX is your 16-bit identifier.

Characteristic Properties

Property Meaning Use Case
Read Client can request current value Get joystick position
Write Client can set value Configure sensitivity
Notify Server pushes updates (no ACK) Button press events
Indicate Server pushes updates (with ACK) Critical status changes

Notify vs Indicate: Notify is faster (no acknowledgment) but less reliable. For button presses, Notify is perfectโ€”if one is lost, the next one comes quickly anyway.

BLE Advertising

Before any connection, the BLE device must advertise its presence. Advertising packets are broadcast on channels 37, 38, and 39.

Advertising Packet (31 bytes max):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Flags โ”‚ Name โ”‚ Service UUIDs โ”‚ Manufacturer Data โ”‚ TX Power โ”‚
โ”‚ (3B)  โ”‚ (var)โ”‚     (var)     โ”‚       (var)       โ”‚   (2B)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Advertising Interval: Time between advertising packets

  • Short interval (20-100ms): Faster discovery, higher power
  • Long interval (1-10s): Slower discovery, lower power

For a game controller, use ~100ms interval while waiting for connection.

Hardware Interrupts

Polling buttons in a loop wastes CPU cycles. Interrupts allow the CPU to sleep until something happens.

Polling (wasteful):                Interrupts (efficient):

while(1) {                         void IRAM_ATTR button_isr() {
  if (button_pressed()) {            // Handle immediately
    handle_button();                  xSemaphoreGiveFromISR(sem);
  }                                }
  delay(10);
}                                  // CPU sleeps until interrupt

ESP32 Interrupt Configuration

gpio_config_t io_conf = {
    .pin_bit_mask = (1ULL << BUTTON_A_PIN),
    .mode = GPIO_MODE_INPUT,
    .pull_up_en = GPIO_PULLUP_ENABLE,
    .intr_type = GPIO_INTR_NEGEDGE,  // Trigger on falling edge
};
gpio_config(&io_conf);

gpio_install_isr_service(0);
gpio_isr_handler_add(BUTTON_A_PIN, button_isr_handler, NULL);

IRAM_ATTR: ISR code must be in IRAM (fast memory) for reliable execution.

Button Debouncing

Mechanical buttons โ€œbounceโ€โ€”the contacts vibrate when pressed, creating multiple electrical transitions.

Ideal button press:          Real button press (bouncing):

     โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€            โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ” โ”Œโ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€
          โ”‚     โ”‚                      โ””โ”€โ”˜ โ””โ”€โ”˜ โ””โ”€โ”€โ”€โ”˜
          โ””โ”€โ”€โ”€โ”€โ”€โ”˜                       โ”‚โ†  ~10ms โ†’โ”‚

Without debouncing, one physical press might register as 5-10 presses!

Software Debouncing Strategy

volatile uint32_t last_interrupt_time = 0;

void IRAM_ATTR button_isr() {
    uint32_t now = xTaskGetTickCountFromISR();

    if (now - last_interrupt_time > pdMS_TO_TICKS(50)) {
        // Real button press
        xSemaphoreGiveFromISR(button_semaphore, NULL);
    }
    last_interrupt_time = now;
}

The 50ms debounce window ignores rapid transitions while still feeling responsive.

ADC for Joystick Input

Joysticks use potentiometersโ€”variable resistors that output a voltage proportional to position.

Joystick Internals:

            VCC (3.3V)
               โ”‚
               โ”ค Potentiometer
               โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ WIPER (to ADC)
               โ”ค
               โ”‚
              GND

Position:    Left      Center      Right
Voltage:     0V        1.65V       3.3V
ADC Value:   0         2048        4095

ADC Calibration

Real joysticks have manufacturing tolerances:

  • Center might be at 2100 instead of 2048
  • Maximum might be 4000 instead of 4095
  • Deadzone exists around center (drift)
typedef struct {
    int16_t center_x;     // Calibrated center (read at startup)
    int16_t center_y;
    int16_t deadzone;     // Ignore movements this small (e.g., 100)
    int16_t max_value;    // Actual max (e.g., 4000)
} joystick_calibration_t;

uint8_t calibrate_joystick_axis(int16_t raw, joystick_calibration_t* cal) {
    // Apply deadzone
    if (abs(raw - cal->center) < cal->deadzone) {
        return 128;  // Center
    }

    // Scale to 0-255
    if (raw < cal->center) {
        return map(raw, 0, cal->center - cal->deadzone, 0, 127);
    } else {
        return map(raw, cal->center + cal->deadzone, cal->max_value, 128, 255);
    }
}

BLE Connection Parameters

Connection parameters significantly affect latency and power consumption.

Connection Interval: 7.5ms to 4s
โ””โ”€โ”€ Time between data exchanges

Slave Latency: 0 to 500
โ””โ”€โ”€ Number of connection events peripheral can skip

Supervision Timeout: 100ms to 32s
โ””โ”€โ”€ Time before connection declared dead

For a game controller:

  • Connection Interval: 7.5-15ms (low latency!)
  • Slave Latency: 0 (never skip events)
  • Supervision Timeout: 2s (detect disconnects quickly)

Project Specification

Hardware Requirements

Component Quantity Purpose
ESP32 DevKit 1 Main microcontroller
Tactile buttons 4-8 A, B, X, Y buttons
Analog joystick 1-2 Directional control
LED (green) 1 Connected indicator
LED (red) 1 Low battery indicator
220ฮฉ Resistor 2 LED current limiting
3.7V LiPo battery 1 Portable power
TP4056 module 1 Battery charging

Wiring Diagram

ESP32 DevKit                    Buttons (active LOW)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚      GPIO14 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ BTN_A  โ”‚โ”€โ”€GND
โ”‚      GPIO27 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ BTN_B  โ”‚โ”€โ”€GND
โ”‚      GPIO26 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ BTN_X  โ”‚โ”€โ”€GND
โ”‚      GPIO25 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ BTN_Y  โ”‚โ”€โ”€GND
โ”‚             โ”‚                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚             โ”‚
โ”‚             โ”‚                 Joystick
โ”‚      GPIO34 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ VRx (X-axis)
โ”‚      GPIO35 โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ VRy (Y-axis)
โ”‚        3.3V โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ VCC
โ”‚         GND โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ GND
โ”‚             โ”‚
โ”‚             โ”‚                 LEDs
โ”‚       GPIO2 โ”‚โ”€โ”€โ”€โ”€[220ฮฉ]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ Green LED โ”‚โ”€โ”€GND
โ”‚       GPIO4 โ”‚โ”€โ”€โ”€โ”€[220ฮฉ]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ Red LED   โ”‚โ”€โ”€GND
โ”‚             โ”‚
โ”‚             โ”‚                 Battery (via TP4056)
โ”‚         VIN โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ OUT+
โ”‚         GND โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ OUT-
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

GATT Service Design

ESP32 GamePad GATT Server
โ”‚
โ”œโ”€โ”€ Generic Access Service (0x1800) [Standard]
โ”‚   โ”œโ”€โ”€ Device Name: "ESP32 GamePad"
โ”‚   โ””โ”€โ”€ Appearance: 0x03C4 (Gamepad)
โ”‚
โ”œโ”€โ”€ Battery Service (0x180F) [Standard]
โ”‚   โ””โ”€โ”€ Battery Level (0x2A19)
โ”‚       โ”œโ”€โ”€ Value: uint8 (0-100%)
โ”‚       โ””โ”€โ”€ Properties: Read, Notify
โ”‚
โ””โ”€โ”€ Controller Service (0xFF01) [Custom]
    โ”œโ”€โ”€ Buttons (0xFF02)
    โ”‚   โ”œโ”€โ”€ Value: uint8 (bitmask)
    โ”‚   โ”‚   Bit 0: A, Bit 1: B, Bit 2: X, Bit 3: Y
    โ”‚   โ””โ”€โ”€ Properties: Read, Notify
    โ”‚
    โ”œโ”€โ”€ Joystick X (0xFF03)
    โ”‚   โ”œโ”€โ”€ Value: uint8 (0=left, 128=center, 255=right)
    โ”‚   โ””โ”€โ”€ Properties: Read, Notify
    โ”‚
    โ””โ”€โ”€ Joystick Y (0xFF04)
        โ”œโ”€โ”€ Value: uint8 (0=up, 128=center, 255=down)
        โ””โ”€โ”€ Properties: Read, Notify

Functional Requirements

  1. BLE Advertising
    • Advertise as โ€œESP32 GamePadโ€ when not connected
    • Include battery service UUID in advertising data
    • LED blinks during advertising
  2. Button Input
    • Detect button presses via hardware interrupts
    • Debounce with 50ms window
    • Send BLE notification within 10ms of press
  3. Joystick Input
    • Sample joystick every 20ms (50 Hz)
    • Apply deadzone (10% around center)
    • Only notify on significant change (>5 units)
  4. Battery Monitoring
    • Read battery voltage via ADC
    • Update characteristic every 60 seconds
    • Light red LED when <20%
  5. Connection Management
    • Request 15ms connection interval
    • Solid green LED when connected
    • Resume advertising on disconnect

Solution Architecture

System Block Diagram

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         ESP32 System                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚
โ”‚  โ”‚   Button     โ”‚    โ”‚   Joystick   โ”‚    โ”‚    BLE       โ”‚      โ”‚
โ”‚  โ”‚   Handler    โ”‚    โ”‚    Task      โ”‚    โ”‚   Stack      โ”‚      โ”‚
โ”‚  โ”‚              โ”‚    โ”‚              โ”‚    โ”‚              โ”‚      โ”‚
โ”‚  โ”‚ - ISR        โ”‚    โ”‚ - Sample ADC โ”‚    โ”‚ - Advertisingโ”‚      โ”‚
โ”‚  โ”‚ - Debounce   โ”‚    โ”‚ - Calibrate  โ”‚    โ”‚ - GATT serverโ”‚      โ”‚
โ”‚  โ”‚ - Update     โ”‚โ”€โ”€โ”€โ†’โ”‚ - Notify     โ”‚โ”€โ”€โ”€โ†’โ”‚ - Notify     โ”‚      โ”‚
โ”‚  โ”‚   bitmask    โ”‚    โ”‚              โ”‚    โ”‚   clients    โ”‚      โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚
โ”‚         โ”‚                   โ”‚                   โ”‚               โ”‚
โ”‚         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚                             โ”‚                                    โ”‚
โ”‚                             โ–ผ                                    โ”‚
โ”‚                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                             โ”‚
โ”‚                    โ”‚   Shared     โ”‚                             โ”‚
โ”‚                    โ”‚   State      โ”‚                             โ”‚
โ”‚                    โ”‚              โ”‚                             โ”‚
โ”‚                    โ”‚ - buttons    โ”‚                             โ”‚
โ”‚                    โ”‚ - joy_x, y   โ”‚                             โ”‚
โ”‚                    โ”‚ - battery    โ”‚                             โ”‚
โ”‚                    โ”‚ - connected  โ”‚                             โ”‚
โ”‚                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                             โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Task Distribution

Task Core Priority Stack Purpose
Button Handler Any High (10) 2KB Respond to interrupts
Joystick Sample 0 Medium (5) 2KB ADC sampling loop
BLE Event Loop 1 Medium (5) 4KB Handle BLE stack
Battery Monitor 0 Low (2) 1KB Periodic ADC read

Notification Flow

Button Press Event:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   ISR    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  Semaphore  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  Queue  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ GPIO    โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ Debounceโ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ Update  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚  BLE    โ”‚
โ”‚ Hardwareโ”‚          โ”‚  Timer  โ”‚             โ”‚ Bitmask โ”‚         โ”‚ Notify  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚                    โ”‚                       โ”‚                    โ”‚
   0ms                  50ms                    51ms                 55ms
                      (debounce)              (process)            (transmit)

Total latency: ~55ms (well under perceptible threshold of 100ms)

Phased Implementation Guide

Phase 1: BLE Advertising (Day 1-2)

Goal: Phone discovers ESP32 in Bluetooth settings

  1. Initialize BLE Stack
    • Enable Bluetooth controller and host
    • Register GATT and GAP callbacks
    • Create GATT database
  2. Configure Advertising
    • Set device name: โ€œESP32 GamePadโ€
    • Include battery service UUID
    • Set advertising interval: 100ms
  3. Verify Discovery
    • Open Bluetooth settings on phone
    • Should see โ€œESP32 GamePadโ€
    • Use nRF Connect to inspect advertising data

Checkpoint: Phone discovers โ€œESP32 GamePadโ€

Phase 2: GATT Services (Day 3-4)

Goal: nRF Connect shows all services and characteristics

  1. Create Battery Service
    • UUID: 0x180F (standard)
    • Add Battery Level characteristic (0x2A19)
    • Enable read and notify
  2. Create Controller Service
    • UUID: 0xFF01 (custom)
    • Add Buttons characteristic (0xFF02)
    • Add Joystick X/Y characteristics (0xFF03, 0xFF04)
    • Enable read and notify on all
  3. Handle Connections
    • Log connect/disconnect events
    • Light green LED on connect
    • Resume advertising on disconnect

Checkpoint: nRF Connect shows all services and can read values

Phase 3: Button Input (Day 5-7)

Goal: Button presses appear in nRF Connect

  1. Configure GPIO Interrupts
    • Set buttons as input with pull-up
    • Trigger on falling edge (button press)
    • Register ISR handlers
  2. Implement Debouncing
    • Track last interrupt time
    • Ignore interrupts within 50ms
    • Use semaphore to signal main task
  3. Update Characteristics
    • Update button bitmask on press/release
    • Send BLE notification
    • Log to serial for debugging

Checkpoint: nRF Connect shows button bitmask changing

Phase 4: Joystick Input (Day 8-9)

Goal: Joystick movement appears in nRF Connect

  1. Sample ADC
    • Configure ADC for GPIO34, GPIO35
    • Sample every 20ms (50 Hz)
    • Apply 11dB attenuation for 0-3.3V range
  2. Calibrate Readings
    • Read center value on startup
    • Calculate deadzone (10%)
    • Map raw values to 0-255 range
  3. Notify on Change
    • Only notify if value changed by >5
    • Update characteristic value
    • Send notification

Checkpoint: Joystick movement updates in real-time in nRF Connect

Phase 5: Battery and Polish (Day 10-14)

Goal: Production-quality controller

  1. Battery Monitoring
    • Read battery voltage via ADC
    • Calculate percentage (4.2V=100%, 3.3V=0%)
    • Update battery characteristic every minute
  2. Connection Parameters
    • Request 15ms connection interval
    • Handle parameter update response
    • Verify latency with timing measurements
  3. Power Optimization
    • Reduce advertising power when connected
    • Use light sleep between samples
    • Measure actual current draw

Testing Strategy

Unit Tests

Component Test Expected Result
GPIO Read button state LOW when pressed
ADC Read joystick center ~2048 (ยฑ100)
BLE Start advertising nRF Connect sees device
BLE Connect Connection callback fires
GATT Read battery Value 0-100
GATT Enable notify Notifications received

Integration Tests

  1. Latency Measurement
    • Press button, measure time until phone receives notification
    • Target: <100ms for gaming applications
  2. Concurrent Input
    • Press multiple buttons while moving joystick
    • All inputs should register without missed events
  3. Reconnection
    • Disconnect phone, verify advertising resumes
    • Reconnect, verify all services work

Stress Testing

  1. Rapid Button Presses
    • Press button 10 times per second
    • All presses should register (after debounce)
  2. Long Duration
    • Run for 8 hours on battery
    • Monitor for memory leaks, disconnects

Common Pitfalls and Debugging

BLE Issues

Problem: Phone doesnโ€™t discover device

  • Ensure advertising is started
  • Check advertising data isnโ€™t too long (31 bytes max)
  • Try restarting Bluetooth on phone
  • Clear phoneโ€™s Bluetooth cache

Problem: Connection drops frequently

  • Increase supervision timeout
  • Check for WiFi interference (same 2.4GHz band)
  • Reduce distance to phone

Problem: Notifications not received

  • Verify client enabled notifications (wrote 0x0001 to CCCD)
  • Check characteristic has Notify property
  • Verify MTU is sufficient for data

Button Issues

Problem: Button presses missed

  • ISR might be too slow (use IRAM_ATTR)
  • Debounce time too long (reduce to 20ms)
  • Semaphore queue full (increase queue size)

Problem: Ghost button presses

  • Electrical noise (add 0.1ยตF capacitor)
  • Debounce time too short (increase to 50ms)
  • Floating input (ensure pull-up enabled)

Joystick Issues

Problem: Joystick drifts when idle

  • Increase deadzone size
  • Calibrate center on startup
  • Add hysteresis to prevent oscillation

Problem: Joystick range limited

  • Calibrate actual min/max values
  • Check potentiometer voltage range
  • Verify ADC attenuation setting

Extensions and Challenges

Beginner Extensions

  1. Haptic Feedback
    • Add vibration motor
    • Trigger on button press
    • Control via BLE write characteristic
  2. Multiple Profiles
    • Store different button mappings
    • Switch with long-press combo
    • Save to NVS

Intermediate Challenges

  1. HID Gamepad Profile
    • Implement standard HID over GATT
    • Works as native gamepad on any device
    • No custom app needed
  2. iOS/Android App
    • Write companion app using React Native
    • Display controller state
    • Configure button mappings

Advanced Challenges

  1. Dual Controller Support
    • Two ESP32 controllers paired to one hub
    • Multiplayer gaming support
    • Synchronized timing
  2. Motion Controls
    • Add MPU6050 accelerometer/gyroscope
    • Send orientation data
    • Gesture recognition

Real-World Connections

Commercial Products

Product BLE Feature Your Project Skill
Nintendo Joy-Con BLE gamepad GATT design, low latency
AirPods Proximity sensing BLE connection management
Tile Tracker BLE beacon Advertising, battery life
Fitbit BLE sync Notification characteristics

Industry Applications

  • Medical Devices: Heart rate monitors, glucose meters
  • Industrial IoT: Sensor beacons, asset tracking
  • Consumer Electronics: Remotes, wearables, smart home

Resources

Official Documentation

Resource URL
ESP-IDF BLE API docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/
Bluetooth SIG GATT bluetooth.com/specifications/specs/gatt-specification/
nRF Connect App nordicsemi.com/Products/Development-tools/nrf-connect-for-mobile

Books

Book Author Relevant Chapters
Getting Started with BLE Kevin Townsend All chapters
Making Embedded Systems Elecia White Ch. 5 (GPIO), Ch. 8 (Interrupts)
The Art of Electronics Horowitz & Hill Section 10.5 (Debouncing)

Self-Assessment Checklist

Fundamentals

  • I can explain the GATT hierarchy (Services, Characteristics, Descriptors)
  • I understand the difference between Read, Write, and Notify operations
  • I can describe why button debouncing is necessary
  • I know why interrupts are preferred over polling

Implementation

  • Phone discovers my device in Bluetooth settings
  • nRF Connect shows all my services correctly
  • Button presses trigger notifications within 100ms
  • Joystick movements are smooth and calibrated
  • Battery percentage updates and shows correctly

Code Quality

  • ISRs are minimal (just set flags/semaphores)
  • No busy-waiting in main code
  • Clean separation between input handling and BLE
  • Reconnection works reliably

Interview Preparation

Be ready to answer these questions:

  1. โ€œExplain how BLE GATT works. Whatโ€™s the difference between a service and characteristic?โ€
    • Service groups related data; characteristic is a single value with properties
  2. โ€œWhy use interrupts instead of polling for buttons?โ€
    • CPU can sleep, lower power, more responsive, no wasted cycles
  3. โ€œHow do you handle button debouncing in software?โ€
    • Ignore transitions within time window (20-50ms) after first edge
  4. โ€œWhat are BLE connection parameters and how do they affect latency?โ€
    • Connection interval, slave latency, supervision timeout; shorter interval = lower latency but higher power
  5. โ€œHow would you implement the standard HID profile for a gamepad?โ€
    • Use HID over GATT service (0x1812), define report descriptor, handle report characteristic

Next Project: P03-multi-sensor-deep-sleep-logger.md - Multi-Sensor Data Logger with Deep Sleep