Chapter 67: BLE MIDI Implementation

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the core principles of MIDI (Musical Instrument Digital Interface) and how it’s transported over BLE.
  • Identify the standard 128-bit UUIDs for the BLE MIDI Service and its MIDI I/O Characteristic.
  • Comprehend the packet structure for sending and receiving MIDI messages over BLE, including timestamps.
  • Configure an ESP32 as a GATT server exposing the BLE MIDI service.
  • Implement the necessary GATT characteristics with appropriate properties (Read, WriteWithoutResponse, Notify).
  • Develop an ESP32 application to send basic MIDI messages (e.g., Note On, Note Off) to a connected BLE MIDI host.
  • Test the ESP32 BLE MIDI device with common host software (DAWs, mobile applications).

Introduction

The Musical Instrument Digital Interface (MIDI) has been the standard protocol for communication between electronic musical instruments, computers, and related audio devices for decades. It allows for the transmission of musical performance data, such as note pitches, velocities, timing, and control signals. Traditionally, MIDI relied on dedicated 5-pin DIN connectors and cables.

With the advent of Bluetooth Low Energy, a wireless MIDI specification was developed, primarily driven by Apple and later adopted by the MIDI Association. BLE MIDI allows instruments, controllers, and music software to communicate wirelessly with low latency and low power consumption. This opens up exciting possibilities for creating untethered musical experiences, innovative controllers, and more portable setups. Imagine a wireless keyboard, a gesture-controlled effects unit, or a compact sensor sending musical data to your laptop or tablet without any cables.

This chapter will guide you through the process of implementing a BLE MIDI peripheral device on an ESP32 using ESP-IDF v5.x. You will learn how to set up the specific BLE service and characteristic required for MIDI communication and how to format and send MIDI messages to a connected host, such as a Digital Audio Workstation (DAW) or a mobile music app.

Theory

MIDI Protocol Basics

Before diving into BLE MIDI, let’s briefly revisit what MIDI data looks like. MIDI messages are typically composed of a status byte followed by one or two data bytes.

Component Description Example (Binary / Hex) Notes
Status Byte Identifies the MIDI message type and channel. MSB is always 1. 1001nnnn (Note On)
0x9n
1000nnnn (Note Off)
0x8n
1011nnnn (Control Change)
0xBn
‘n’ represents the MIDI channel (0-15, encoded in the lower 4 bits).
Data Byte 1 First data parameter for the message. MSB is always 0. For Note On/Off: Note Number (0-127)
e.g., 00111100 (60) for C4

For CC: Controller Number (0-127)
e.g., 00000001 (1) for Mod Wheel
Meaning depends on the Status Byte.
Data Byte 2 Second data parameter (if applicable). MSB is always 0. For Note On: Velocity (0-127)
e.g., 01100100 (100)

For Note Off: Velocity (often 0 or 64)
e.g., 01000000 (64)

For CC: Controller Value (0-127)
Not all MIDI messages have a second data byte (e.g., Program Change).
Running Status If multiple messages of the same type and channel are sent consecutively, the Status Byte can be omitted for subsequent messages. N/A (Protocol feature) Helps reduce data transmission size.
System Messages Do not use channels. Include System Common, System Real-Time (e.g., Timing Clock 0xF8), and System Exclusive (SysEx). SysEx Start: 0xF0
SysEx End: 0xF7
Timing Clock: 0xF8
System Real-Time messages can be interleaved.
  • Status Byte: The most significant bit (MSB) is always 1. The lower nibble (4 bits) defines the message type (e.g., Note On, Note Off, Control Change), and the upper nibble (excluding the MSB) often specifies the MIDI channel (0-15).
    • Example: 0x9n is Note On on channel n. 0x8n is Note Off on channel n. 0xBn is Control Change on channel n.
  • Data Bytes: The MSB is always 0.
    • For Note On/Off: Data Byte 1 is the note number (0-127), Data Byte 2 is the velocity (0-127).
    • For Control Change: Data Byte 1 is the controller number (0-127), Data Byte 2 is the controller value (0-127).

Running Status: If multiple messages of the same type are sent consecutively on the same channel, the status byte can be omitted for subsequent messages until a different status byte is sent.

System Messages: There are also system messages (System Common, System Real-Time, System Exclusive – SysEx) that don’t use channels. System Real-Time messages (e.g., Timing Clock 0xF8, Start 0xFA, Stop 0xFC) can be interleaved within other messages.

BLE MIDI Specification

The official BLE MIDI specification is maintained by the MIDI Association, building upon early work by Apple. It defines how MIDI messages are transported over a BLE GATT connection.

A BLE MIDI device acts as a GATT server and exposes a specific service and characteristic.

GATT Entity Name UUID (Standard Format) Purpose & Key Properties
Service MIDI Service 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 Identifies the peripheral as a BLE MIDI capable device. This UUID should be advertised.
Characteristic MIDI Data I/O Characteristic 7772E5DB-3868-4112-A1A9-F2669D106BF3 The channel for sending and receiving MIDI data packets.
Properties:
READ: Allows host to read (less common for streaming MIDI).
WRITE WITHOUT RESPONSE: Allows host to send MIDI messages to the peripheral.
NOTIFY: Allows peripheral to send MIDI messages to the host (primary method for device-to-host data). Requires CCCD.
Descriptor Client Characteristic Configuration Descriptor (CCCD) 0x2902 (Standard 16-bit UUID) Associated with the MIDI Data I/O Characteristic. Allows the host to enable/disable notifications.
– Value 0x0001: Notifications enabled.
– Value 0x0000: Notifications disabled.
  1. MIDI Service
    • UUID: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 (Custom 128-bit UUID)
    • Purpose: To identify the device as a BLE MIDI capable peripheral.
  2. MIDI Data I/O Characteristic
    • UUID: 7772E5DB-3868-4112-A1A9-F2669D106BF3 (Custom 128-bit UUID)
    • Properties:
      • READ: Allows the host to read the characteristic (less common for MIDI messages).
      • WRITE WITHOUT RESPONSE: Allows the host to send MIDI messages to the peripheral.
      • NOTIFY: Allows the peripheral to send MIDI messages to the host. This is the primary way MIDI data flows from the instrument/controller to the host. A CCCD (Client Characteristic Configuration Descriptor, UUID 0x2902) is required for notifications.
    • Value Format (for Notifications from Peripheral to Host):MIDI messages sent over BLE are encapsulated in packets that include timestamps. This helps the host reconstruct the precise timing of musical events. A BLE MIDI packet has the following structure:
      • Byte 0: Header Byte
        • Bits 7-1: 0b10xxxxxx (Bit 7 is always 1, Bit 6 indicates the start of a MIDI message or a continuation of a SysEx message). For typical short MIDI messages, it’s 0b10000000 (0x80) plus the high 6 bits of the timestamp.
        • More precisely, Bit 7 is ‘1’. Bit 6 is ‘0’ for non-SysEx messages. Bits 0-5 are the 6 most significant bits of the 13-bit millisecond timestamp.
        • So, Header = 0x80 | (timestamp_ms >> 7 & 0x3F)
      • Byte 1: Timestamp Low Byte
        • Bit 7: 0b1xxxxxxx (Always 1).
        • Bits 0-6: The 7 least significant bits of the 13-bit millisecond timestamp.
        • So, Timestamp Low = 0x80 | (timestamp_ms & 0x7F)
      • Byte 2 onwards: MIDI Message(s)
        • The actual MIDI message bytes (Status Byte, Data Byte 1, Data Byte 2).
        • Multiple MIDI messages can be packed into a single BLE packet if they share the same timestamp.
        • System Real-Time messages (1 byte each, e.g., 0xF8 for MIDI Clock) can be inserted before any status byte or between data bytes of a multi-byte MIDI message. They are timestamped using the preceding timestamp bytes.
      Timestamps:The timestamp is a 13-bit value representing milliseconds, typically derived from a local timer on the peripheral that starts (or wraps) when the BLE connection is established or when the MIDI functionality is enabled. It rolls over every 8192 ms (2^13 ms).Example Packet for a Note On message:Assume timestamp_ms = 1000 (0x03E8).
      • timestamp_ms >> 7 & 0x3F = 0x03E8 >> 7 & 0x3F = 0x07 & 0x3F = 0x07
      • Header = 0x80 | 0x07 = 0x87
      • timestamp_ms & 0x7F = 0x03E8 & 0x7F = 0x68
      • Timestamp Low = 0x80 | 0x68 = 0xE8
      • MIDI Message: Note On, Channel 1, Note C4 (60), Velocity 100: 0x90, 0x3C, 0x64
      • BLE Packet: [0x87, 0xE8, 0x90, 0x3C, 0x64]
Byte Position Name Bit Structure / Content Description & Calculation (for a 13-bit timestamp_ms)
Byte 0 Header Byte 10xxxxxx (Bit 7 always 1, Bit 6 usually 0 for non-SysEx) Contains the 6 Most Significant Bits (MSB) of the 13-bit millisecond timestamp.
Value: 0x80 | ((timestamp_ms >> 7) & 0x3F)
Byte 1 Timestamp Low Byte 1xxxxxxx (Bit 7 always 1) Contains the 7 Least Significant Bits (LSB) of the 13-bit millisecond timestamp.
Value: 0x80 | (timestamp_ms & 0x7F)
Byte 2 MIDI Status Byte 1nnnnccc The first byte of the actual MIDI message (e.g., 0x90 for Note On, channel 0).
Byte 3 (Optional) MIDI Data Byte 1 0ddddddd The second byte of the MIDI message (e.g., Note Number).
Byte 4 (Optional) MIDI Data Byte 2 0vvvvvvv The third byte of the MIDI message (e.g., Velocity).
Notes:
  • The 13-bit timestamp typically represents milliseconds since connection or a defined epoch, and wraps every 8192 ms.
  • Multiple MIDI messages can be packed into one BLE packet if they share the same timestamp header (Byte 0 and Byte 1).
  • System Real-Time messages (1 byte each, e.g., 0xF8 MIDI Clock) can be inserted before any MIDI status byte or between data bytes of a multi-byte MIDI message, sharing the preceding timestamp.
  • For SysEx messages, Bit 6 of the Header Byte and subsequent bytes have special meanings for start/continuation/end.
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph LR
    subgraph BLE_Packet [BLE MIDI Packet]
        direction LR
        HEAD["Header Byte<br><br><b>0x80 | (TS_High_6_bits)</b><br><em>(e.g., 0x87)</em>"]:::header
        TS_LOW["Timestamp Low Byte<br><br><b>0x80 | (TS_Low_7_bits)</b><br><em>(e.g., 0xE8)</em>"]:::timestamp
        
        subgraph MIDI_Payload ["MIDI Message(s)"]
            direction LR
            MIDI_S["MIDI Status<br><em>(e.g., 0x90)</em>"]:::midi_status
            MIDI_D1["MIDI Data 1<br><em>(e.g., 0x3C)</em>"]:::midi_data
            MIDI_D2["MIDI Data 2<br><em>(e.g., 0x64)</em>"]:::midi_data
            
            subgraph Optional_Interleaved [Optional Interleaved Data]
              direction LR
              style Optional_Interleaved fill:#transparent,stroke:#ccc,stroke-dasharray: 5 5
              MIDI_RT["System Real-Time<br><em>(e.g., 0xF8)</em>"]:::midi_realtime
              MIDI_CONT[More MIDI Data<br>or New Message]:::midi_data
            end
        end
        HEAD --> TS_LOW;
        TS_LOW --> MIDI_S;
        MIDI_S --> MIDI_D1;
        MIDI_D1 --> MIDI_D2;
        MIDI_D2 --> Optional_Interleaved;
    end

    subgraph TIMING ["Timestamp (13-bit Millisecond Value)"]
        TS_VAL["Timestamp_MS<br><em>(e.g., 1000ms = 0x03E8)</em>"]:::timestamp_val
        TS_VAL --> |Split into| HEAD
        TS_VAL --> |Split into| TS_LOW
    end
    
    classDef header fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef timestamp fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef midi_status fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef midi_data fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef midi_realtime fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    classDef timestamp_val fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3;
    classDef packet_group fill:#F3F4F6,stroke:#6B7280,stroke-width:1px,color:#1F2937;

    class BLE_Packet packet_group;
    class MIDI_Payload packet_group;
    class TIMING packet_group;

Connection and Security

  • Advertising: The BLE MIDI peripheral should advertise the 128-bit MIDI Service UUID.
  • Pairing/Bonding: While not strictly mandated by the BLE MIDI spec to be authenticated for basic operation, it’s good practice to implement security (e.g., Just Works pairing with bonding, or authenticated pairing if sensitive SysEx data might be exchanged). Encryption protects the MIDI data stream.
  • MTU (Maximum Transmission Unit): The default ATT MTU is 23 bytes. This means a BLE packet can carry up to MTU - 3 = 20 bytes of attribute data. A typical MIDI packet (Header, TS Low, 3-byte MIDI message) is 5 bytes, so it fits easily. For longer messages like SysEx, they need to be fragmented according to BLE MIDI rules (using specific header bits and potentially multiple BLE packets).

Operation Flow (ESP32 as BLE MIDI Peripheral)

  1. Initialization: Standard BLE setup (NVS, controller, Bluedroid).
  2. GATT Server Setup:
    • Register a GATTS callback.
    • Create the MIDI Service using its 128-bit UUID.
    • Add the MIDI Data I/O Characteristic using its 128-bit UUID, with properties Read, WriteWithoutResponse, and Notify.
    • Add a CCCD (Client Characteristic Configuration Descriptor) to the MIDI Data I/O Characteristic to allow clients to enable/disable notifications.
  3. Advertising:
    • Configure advertising data to include the MIDI Service UUID.
    • Set device name and appearance (optional, but good for user experience).
    • Start advertising.
  4. Connection Handling:
    • On connection, store conn_id and gatts_if.
    • Handle pairing/bonding requests if security is implemented.
  5. MIDI Message Transmission:
    • When a musical event occurs (e.g., button press, sensor reading):
      1. Get the current millisecond timestamp (relative to connection start or a fixed point).
      2. Format the MIDI message (e.g., Note On).
      3. Construct the BLE MIDI packet: Header byte, Timestamp Low byte, MIDI message bytes.
      4. If a client has subscribed to notifications (CCCD is 0x0001), send the packet using esp_ble_gatts_send_indicate() or esp_ble_gatts_send_notify() (notify is more common for MIDI). esp_ble_gatts_send_indicate requires confirmation from the client, esp_ble_gatts_send_notify does not. For MIDI, notify is generally preferred for lower latency.
  6. MIDI Message Reception (from Host to Peripheral):
    • In the ESP_GATTS_WRITE_EVT, if the write is to the MIDI Data I/O Characteristic handle:
      1. Parse the incoming BLE MIDI packet (Header, Timestamp Low, MIDI messages).
      2. Process the MIDI messages (e.g., control an LED, change a synth parameter).
      • Note: Timestamps in messages from the host are relative to the host’s clock.
%%{ init: { "theme": "base", "themeVariables": { "fontFamily": "Open Sans" } } }%%
graph TD
    A["Start: ESP32 Power On"]:::primary --> B{"Initialize NVS, BT Controller, Bluedroid"}
    B -- "Success" --> C["Register GATTS Callback"]
    C -- "Success" --> D["Register GATTS Application Profile"]
    
    subgraph GATT_Setup ["GATT Service & Characteristic Creation (on ESP_GATTS_REG_EVT)"]
        direction TB
        D1["Create MIDI Service (UUID: 03B8...)"]:::process
        D1 --> D2["Add MIDI Data I/O Characteristic (UUID: 7772...<br>Props: Read, WriteNR, Notify)"]:::process
        D2 --> D3["Add CCCD to MIDI Data I/O Char."]:::process
        D3 --> D4["Start MIDI Service"]:::process
    end
    D --> D1

    D4 -- Service Started --> E[Configure Advertising Data:<br>Name, MIDI Service UUID]
    E -- Success --> F[Start Advertising]
    
    F --> G{Host Scans & Discovers ESP32}
    G --> H[Host Initiates Connection]
    H --> I{Connection Established}
    I -- Optional Security --> J[Pairing & Bonding Process]
    J --> K{"Secure Encrypted Link (if paired)"}
    I --> K_NoSec[Unencrypted Link]

    K --> L[Host Discovers Services & Characteristics]
    K_NoSec --> L
    L --> M[Host Writes to CCCD to Enable Notifications for MIDI Data I/O]
    
    M --> N{"Musical Event on ESP32<br>(e.g., Button Press, Sensor Input)"}
    N --> O["Get Current Timestamp (13-bit ms)"]
    O --> P["Format MIDI Message (e.g., Note On)"]
    P --> Q[Construct BLE MIDI Packet:<br>Header, TS_Low, MIDI_Data]
    Q --> R{Client Subscribed?}
    R -- Yes --> S["Call esp_ble_gatts_send_notify() with Packet"]
    S -- Data Sent --> N
    
    subgraph Loop ["Normal MIDI Sending Operation"]
        N
        O
        P
        Q
        R
        S
    end
    
    R -- No --> N

    I --> T_DIS1{Host or Device Initiates Disconnect}
    M --> T_DIS2{Link Lost / Disconnect}
    T_DIS1 --> U[Disconnection Event]
    T_DIS2 --> U
    U --> F_RE_ADV[Re-advertise or Low Power State]
    
    subgraph HOST_INTERACTION ["Host Interaction"]
        direction LR
        G
        H
        L
        M
    end

    classDef primary fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E
    classDef success fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46
    classDef io fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B
    classDef state fill:#E0E7FF,stroke:#4338CA,stroke-width:1px,color:#3730A3

    class A primary
    class B,C,D,D1,D2,D3,D4,E,F,I,J,K,K_NoSec,L,M,N,O,P,Q,S,U,F_RE_ADV process
    class G,H io
    class Loop,HOST_INTERACTION,GATT_Setup state
    class R,T_DIS1,T_DIS2 decision

Practical Examples

Prerequisites:

  • An ESP32 board (ESP32, ESP32-C3, ESP32-S3, ESP32-C6, ESP32-H2).
  • VS Code with the Espressif IDF Extension.
  • A BLE MIDI host application:
    • macOS: GarageBand, Logic Pro X, Ableton Live, Cubase (all have built-in BLE MIDI support).
    • iOS: GarageBand, numerous synth apps.
    • Windows: Requires a third-party utility like “MIDIberry” or a DAW with native BLE MIDI support (e.g., Cakewalk by BandLab, recent Cubase/Ableton versions).
    • Android: Apps like “MIDI BLE Connect” and some DAWs/synth apps.
  • (Optional) A physical button connected to a GPIO for triggering MIDI notes.

Example 1: ESP32 as a BLE MIDI Note Sender

This example configures the ESP32 to act as a simple BLE MIDI device. It will periodically send MIDI Note On and Note Off messages for a C4 note to any connected and subscribed host.

1. Project Setup:

  • Create a new ESP-IDF project: idf.py create-project ble_midi_sender
  • cd ble_midi_sender
  • idf.py menuconfig:
    • Component config -> Bluetooth -> Bluetooth (Enable)
    • Component config -> Bluetooth -> Bluetooth Host -> Bluedroid (Select)
    • Component config -> Bluetooth -> BLE Only (Enable)
    • Component config -> Bluetooth -> GATT -> CONFIG_BT_GATT_MAX_SR_PROFILES (Set to at least 1, or more if you have other services)
    • (Optional) Component config -> Log output -> Default log verbosity to Info or Debug.

2. Code (main/ble_midi_main.c):

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_gatt_common_api.h" // For esp_gatt_perm_t

#define MIDI_TAG "BLE_MIDI"
#define DEVICE_NAME "ESP32-BLE-MIDI"

// BLE MIDI Service UUID: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700
static const uint8_t midi_service_uuid[16] = {
    0x00, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7,
    0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03
};

// BLE MIDI Characteristic UUID: 7772E5DB-3868-4112-A1A9-F2669D106BF3
static const uint8_t midi_char_uuid[16] = {
    0xF3, 0x6B, 0x10, 0x9D, 0x66, 0xF2, 0xA9, 0xA1,
    0x12, 0x41, 0x68, 0x38, 0xDB, 0xE5, 0x72, 0x77
};

// GATT Profile and Attribute Handle Management
#define PROFILE_NUM 1
#define PROFILE_APP_ID 0
#define SVC_INST_ID 0

enum {
    IDX_SVC,
    IDX_MIDI_CHAR,      // MIDI Data I/O Characteristic Declaration
    IDX_MIDI_VAL,       // MIDI Data I/O Characteristic Value
    IDX_MIDI_CCC,       // MIDI Data I/O CCCD

    HRS_IDX_NB,
};
uint16_t midi_handle_table[HRS_IDX_NB];

static uint8_t current_midi_value[20] = {0}; // Max MTU - 3 for attribute value

// Client connection information
typedef struct {
    esp_gatt_if_t gatts_if;
    uint16_t conn_id;
    bool notifications_enabled;
    uint32_t connection_start_time_ms; // For timestamps
} client_conn_info_t;

static client_conn_info_t connected_client = {0};

// Advertising data
static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp = false,
    .include_name = true,
    .include_txpower = true,
    .min_interval = 0x0020, // slave connection min interval, Time = min_interval * 1.25 msec
    .max_interval = 0x0040, // slave connection max interval, Time = max_interval * 1.25 msec
    .appearance = 0x00, // No specific appearance
    .manufacturer_len = 0,
    .p_manufacturer_data = NULL,
    .service_data_len = 0,
    .p_service_data = NULL,
    .service_uuid_len = sizeof(midi_service_uuid), // Length of 128-bit UUID
    .p_service_uuid = (uint8_t*)midi_service_uuid, // Pointer to service UUID
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};

// Advertising parameters
static esp_ble_adv_params_t adv_params = {
    .adv_int_min = 0x20, // 32 slots * 0.625ms = 20ms
    .adv_int_max = 0x40, // 64 slots * 0.625ms = 40ms
    .adv_type = ADV_TYPE_IND,
    .own_addr_type = BLE_ADDR_TYPE_PUBLIC,
    .channel_map = ADV_CHNL_ALL,
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

// Function to send MIDI message
static void send_midi_message(uint8_t *midi_data, uint8_t len) {
    if (!connected_client.notifications_enabled || connected_client.conn_id == 0) {
        ESP_LOGW(MIDI_TAG, "Client not connected or notifications not enabled.");
        return;
    }

    uint32_t current_time_ms = esp_log_timestamp(); // esp_log_timestamp() gives ms since boot
    // For more accurate relative timing, use time since connection
    // uint32_t timestamp_ms = current_time_ms - connected_client.connection_start_time_ms;
    uint32_t timestamp_ms = (current_time_ms - connected_client.connection_start_time_ms) & 0x1FFF; // 13-bit timestamp

    uint8_t packet[len + 2]; // Header + Timestamp Low + MIDI data

    // Header Byte: Bit 7 is 1, Bit 6 is 0. Bits 0-5 are MSB of timestamp.
    packet[0] = 0x80 | ((timestamp_ms >> 7) & 0x3F);
    // Timestamp Low Byte: Bit 7 is 1. Bits 0-6 are LSB of timestamp.
    packet[1] = 0x80 | (timestamp_ms & 0x7F);

    memcpy(&packet[2], midi_data, len);

    esp_err_t ret = esp_ble_gatts_send_indicate(connected_client.gatts_if,
                                              connected_client.conn_id,
                                              midi_handle_table[IDX_MIDI_VAL],
                                              sizeof(packet), packet, false); // false for notification
    if (ret != ESP_OK) {
        ESP_LOGE(MIDI_TAG, "Error sending MIDI notification: %s", esp_err_to_name(ret));
    } else {
        ESP_LOGI(MIDI_TAG, "Sent MIDI packet, TS: %lu ms", timestamp_ms);
        esp_log_buffer_hex(MIDI_TAG, packet, sizeof(packet));
    }
}

// Task to send MIDI Note On/Off periodically
void midi_send_task(void *pvParameters) {
    uint8_t note_on_c4[] = {0x90, 60, 100}; // Note On, Channel 1, C4 (Middle C), Velocity 100
    uint8_t note_off_c4[] = {0x80, 60, 0};   // Note Off, Channel 1, C4, Velocity 0

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(2000)); // Every 2 seconds
        if (connected_client.notifications_enabled) {
            ESP_LOGI(MIDI_TAG, "Sending Note ON C4");
            send_midi_message(note_on_c4, sizeof(note_on_c4));
            vTaskDelay(pdMS_TO_TICKS(500)); // Hold note for 0.5 second
            ESP_LOGI(MIDI_TAG, "Sending Note OFF C4");
            send_midi_message(note_off_c4, sizeof(note_off_c4));
        }
    }
}

static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
    switch (event) {
    case ESP_GATTS_REG_EVT:
        ESP_LOGI(MIDI_TAG, "REG_EVT, status %d, app_id %d, gatts_if %d", param->reg.status, param->reg.app_id, gatts_if);
        if (param->reg.status == ESP_GATT_OK) {
            connected_client.gatts_if = gatts_if; // Store for this profile
        } else {
            ESP_LOGE(MIDI_TAG, "GATTS App registration failed"); return;
        }

        esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(DEVICE_NAME);
        if (set_dev_name_ret){ ESP_LOGE(MIDI_TAG, "set device name failed, error code = %x", set_dev_name_ret); }
        
        esp_err_t adv_config_ret = esp_ble_gap_config_adv_data(&adv_data);
        if (adv_config_ret){ ESP_LOGE(MIDI_TAG, "config adv data failed, error code = %x", adv_config_ret); }

        // Create MIDI Service
        esp_gatts_attr_db_t midi_service_db[HRS_IDX_NB] = {
            // Service Declaration
            [IDX_SVC] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&ESP_GATT_UUID_PRI_SERVICE, ESP_GATT_PERM_READ,
                                             sizeof(midi_service_uuid), sizeof(midi_service_uuid), (uint8_t *)midi_service_uuid}},
            // MIDI Data I/O Characteristic Declaration
            [IDX_MIDI_CHAR] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&ESP_GATT_UUID_CHAR_DECLARE, ESP_GATT_PERM_READ,
                                             ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR | ESP_GATT_CHAR_PROP_BIT_NOTIFY, // Properties
                                             0, 0, NULL}},
            // MIDI Data I/O Characteristic Value
            [IDX_MIDI_VAL] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_128, (uint8_t *)midi_char_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, // Permissions
                                             sizeof(current_midi_value), sizeof(current_midi_value), current_midi_value}},
            // MIDI Data I/O CCCD
            [IDX_MIDI_CCC] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&ESP_GATT_UUID_CHAR_CLIENT_CONFIG, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
                                             sizeof(uint16_t), sizeof(uint16_t), (uint8_t *)(uint16_t[]){0x0000} }}, // Initial value 0 (notifications disabled)
        };
        esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(midi_service_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
        if (create_attr_ret) {
            ESP_LOGE(MIDI_TAG, "create attr table failed, error code = %x", create_attr_ret);
        }
        break;

    case ESP_GATTS_CREAT_ATTR_TAB_EVT:
        ESP_LOGI(MIDI_TAG, "The number handle = %x",param->add_attr_tab.num_handle);
        if (param->add_attr_tab.status == ESP_GATT_OK){
            if(param->add_attr_tab.num_handle == HRS_IDX_NB) { // Check if all handles were created
                memcpy(midi_handle_table, param->add_attr_tab.handles, sizeof(midi_handle_table));
                esp_ble_gatts_start_service(midi_handle_table[IDX_SVC]);
                ESP_LOGI(MIDI_TAG, "Service started, handles assigned.");
            } else {
                ESP_LOGE(MIDI_TAG, "Create attribute table abnormally, num_handle (%d) doesn't equal HRS_IDX_NB(%d)",
                         param->add_attr_tab.num_handle, HRS_IDX_NB);
            }
        } else {
            ESP_LOGE(MIDI_TAG, "Create attribute table failed, error code = %x", param->add_attr_tab.status);
        }
        break;

    case ESP_GATTS_START_EVT:
        ESP_LOGI(MIDI_TAG, "SERVICE_START_EVT, status %d, service_handle %d", param->start.status, param->start.service_handle);
        break;

    case ESP_GATTS_CONNECT_EVT:
        ESP_LOGI(MIDI_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, gatts_if %d",
                 param->connect.conn_id,
                 param->connect.remote_bda[0], param->connect.remote_bda[1], param->connect.remote_bda[2],
                 param->connect.remote_bda[3], param->connect.remote_bda[4], param->connect.remote_bda[5],
                 gatts_if);
        connected_client.conn_id = param->connect.conn_id;
        connected_client.gatts_if = gatts_if; // Update gatts_if for this connection
        connected_client.notifications_enabled = false;
        connected_client.connection_start_time_ms = esp_log_timestamp();
        // Stop advertising on connection if desired, or allow multiple connections
        // esp_ble_gap_stop_advertising();
        // Security request
        esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM);
        break;

    case ESP_GATTS_DISCONNECT_EVT:
        ESP_LOGI(MIDI_TAG, "DISCONNECT_EVT, conn_id %d, reason %d", param->disconnect.conn_id, param->disconnect.reason);
        connected_client.conn_id = 0;
        connected_client.notifications_enabled = false;
        // Restart advertising to allow new connections
        esp_ble_gap_start_advertising(&adv_params);
        break;

    case ESP_GATTS_WRITE_EVT:
        ESP_LOGI(MIDI_TAG, "WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d, len %d, need_rsp %d",
                 param->write.conn_id, param->write.trans_id, param->write.handle, param->write.len, param->write.need_rsp);
        esp_log_buffer_hex(MIDI_TAG, param->write.value, param->write.len);

        if (param->write.handle == midi_handle_table[IDX_MIDI_CCC] && param->write.len == 2) {
            uint16_t cccd_val = (param->write.value[1] << 8) | param->write.value[0];
            if (cccd_val == 0x0001) {
                ESP_LOGI(MIDI_TAG, "Notifications ENABLED for MIDI Data");
                connected_client.notifications_enabled = true;
            } else if (cccd_val == 0x0000) {
                ESP_LOGI(MIDI_TAG, "Notifications DISABLED for MIDI Data");
                connected_client.notifications_enabled = false;
            } else {
                ESP_LOGE(MIDI_TAG, "Invalid CCCD value: 0x%04x", cccd_val);
            }
        } else if (param->write.handle == midi_handle_table[IDX_MIDI_VAL]) {
            ESP_LOGI(MIDI_TAG, "MIDI Data received from host:");
            esp_log_buffer_hex(MIDI_TAG, param->write.value, param->write.len);
            // TODO: Parse and handle incoming MIDI data from host
            // For example, if byte 2 is 0x9x, it's a Note On.
        }
        // Send response if needed (this example uses auto_rsp for CCCD and MIDI value)
        if (param->write.need_rsp && param->write.is_prep == false) { // Check is_prep for long writes
             esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
        }
        break;
    
    case ESP_GATTS_CONF_EVT: // Confirmation for indication (if using send_indicate with true)
        ESP_LOGI(MIDI_TAG, "CONF_EVT, status %d, handle %d", param->conf.status, param->conf.handle);
        if (param->conf.status != ESP_GATT_OK) {
            esp_log_buffer_hex(MIDI_TAG, param->conf.value, param->conf.len);
        }
        break;
    
    // Other GATTS events like READ, MTU, etc.
    case ESP_GATTS_MTU_EVT:
        ESP_LOGI(MIDI_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);
        break;

    default:
        ESP_LOGD(MIDI_TAG, "Unhandled GATTS event: %d", event);
        break;
    }
}

static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
    switch (event) {
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
        ESP_LOGI(MIDI_TAG, "Advertising data set complete");
        esp_ble_gap_start_advertising(&adv_params);
        break;
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
        if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
            ESP_LOGI(MIDI_TAG, "Advertising started successfully");
        } else {
            ESP_LOGE(MIDI_TAG, "Advertising start failed, error status = %x", param->adv_start_cmpl.status);
        }
        break;
    case ESP_GAP_BLE_SEC_REQ_EVT:
        ESP_LOGI(MIDI_TAG, "ESP_GAP_BLE_SEC_REQ_EVT");
        esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
        break;
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
        if (param->ble_security.auth_cmpl.success) {
            ESP_LOGI(MIDI_TAG, "Authentication complete: bd_addr %02x:%02x:%02x:%02x:%02x:%02x, key_type 0x%x",
                     param->ble_security.auth_cmpl.bd_addr[0], param->ble_security.auth_cmpl.bd_addr[1],
                     param->ble_security.auth_cmpl.bd_addr[2], param->ble_security.auth_cmpl.bd_addr[3],
                     param->ble_security.auth_cmpl.bd_addr[4], param->ble_security.auth_cmpl.bd_addr[5],
                     param->ble_security.auth_cmpl.key_type);
        } else {
            ESP_LOGE(MIDI_TAG, "Authentication failed: reason 0x%x", param->ble_security.auth_cmpl.fail_reason);
        }
        break;
    default:
        ESP_LOGD(MIDI_TAG, "Unhandled GAP event: %d", event);
        break;
    }
}


void app_main(void) {
    esp_err_t ret;

    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    ret = esp_bt_controller_init(&bt_cfg);
    if (ret) { ESP_LOGE(MIDI_TAG, "Initialize controller failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
    if (ret) { ESP_LOGE(MIDI_TAG, "Enable controller failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bluedroid_init();
    if (ret) { ESP_LOGE(MIDI_TAG, "Init Bluedroid failed: %s", esp_err_to_name(ret)); return; }
    ret = esp_bluedroid_enable();
    if (ret) { ESP_LOGE(MIDI_TAG, "Enable Bluedroid failed: %s", esp_err_to_name(ret)); return; }

    ret = esp_ble_gatts_register_callback(gatts_profile_event_handler);
    if (ret) { ESP_LOGE(MIDI_TAG, "GATTS register error: %s", esp_err_to_name(ret)); return; }
    ret = esp_ble_gap_register_callback(gap_event_handler);
    if (ret) { ESP_LOGE(MIDI_TAG, "GAP register error: %s", esp_err_to_name(ret)); return; }
    
    ret = esp_ble_gatts_app_register(PROFILE_APP_ID); // Single profile
    if (ret) { ESP_LOGE(MIDI_TAG, "GATTS app register error: %s", esp_err_to_name(ret)); return; }

    // Set up security parameters - Just Works pairing with MITM protection if possible
    esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; // Secure Connections, MITM, Bonding
    esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; // No input/output capabilities on ESP32 for this example
    esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t));
    esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
    // uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; // Distribute encryption and identity keys
    // esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t));
    // uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK;  // Accept encryption and identity keys
    // esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t));

    esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500); // Set local MTU to a larger value
    if (local_mtu_ret){
        ESP_LOGE(MIDI_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
    }
    
    xTaskCreate(midi_send_task, "midi_send_task", 2048, NULL, 5, NULL);
    
    ESP_LOGI(MIDI_TAG, "BLE MIDI Sender Initialized. Advertising will start after GATTS registration.");
}

Important Note on UUID Byte Order: When defining 128-bit UUIDs as byte arrays in C, they are typically written in reverse order (least significant byte first) compared to how they are commonly displayed (most significant byte first). The code above has the UUIDs in the correct LSB-first order for esp_ble_gatts_create_attr_tab and advertising data.

  • MIDI Service: 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 becomes {0x00, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7, 0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03}
  • MIDI Characteristic: 7772E5DB-3868-4112-A1A9-F2669D106BF3 becomes {0xF3, 0x6B, 0x10, 0x9D, 0x66, 0xF2, 0xA9, 0xA1, 0x12, 0x41, 0x68, 0x38, 0xDB, 0xE5, 0x72, 0x77}

3. Build, Flash, and Monitor:

  • idf.py build
  • idf.py -p (PORT) flash monitor (Replace (PORT) with your ESP32’s serial port)

4. Observe Output & Test with BLE MIDI Host:

  • Serial Monitor: You should see logs for Bluetooth initialization, GATTS registration, service creation, advertising start, and connection events. When a host connects and enables notifications, you’ll see “Notifications ENABLED” and logs for sent MIDI packets.
  • BLE MIDI Host Application:
    1. Open your chosen BLE MIDI host software (e.g., GarageBand on macOS/iOS, MIDI BLE Connect on Android).
    2. Look for a way to connect to Bluetooth MIDI devices. This is often in the MIDI settings or preferences of the application.
    3. You should see “ESP32-BLE-MIDI” listed. Connect to it.
    4. The host application should automatically discover the MIDI service/characteristic and enable notifications.
    5. Once connected, you should start receiving MIDI Note On/Off messages for C4 every few seconds. If your DAW is set up with a virtual instrument on the track receiving MIDI from the ESP32, you should hear the note play.

Tip: The esp_log_timestamp() function provides milliseconds since boot. For more accurate inter-event timing, it’s better to calculate timestamps relative to the BLE connection establishment or a specific start event for your MIDI stream, ensuring the 13-bit timestamp wraps correctly. The example uses (current_time_ms - connected_client.connection_start_time_ms) & 0x1FFF; for this.

Variant Notes

  • ESP32, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: All these variants have full Bluetooth LE support and can implement BLE MIDI services as described. The ESP-IDF GATT APIs are consistent across these BLE-enabled chips.
  • ESP32-S2: This variant does not have Bluetooth hardware and therefore cannot implement BLE MIDI services. It can, however, implement USB MIDI if needed, using its USB OTG controller.

The BLE MIDI implementation is primarily a software concern at the GATT and application level, making it portable across ESP32 variants that have BLE capabilities.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect Service/Characteristic UUIDs or Byte Order Host device (DAW, mobile app) cannot find or recognize the ESP32 as a BLE MIDI device. It might connect as a generic BLE device but MIDI functionality is absent. Verify UUIDs: Double-check the official 128-bit BLE MIDI Service UUID (03B80E5A-EDE8-4B33-A751-6CE34EC4C700) and MIDI Data I/O Characteristic UUID (7772E5DB-3868-4112-A1A9-F2669D106BF3).
Check Byte Order: Ensure UUIDs are in LSB-first byte order in C arrays for ESP-IDF functions (e.g., {0x00, 0xC7,…,0x03} for the service).
Use BLE Scanner: Use a generic BLE scanner app (like nRF Connect) to verify the advertised service UUID and discovered characteristic UUIDs.
MIDI Packet Formatting Errors Host receives BLE data but interprets it as garbled or invalid MIDI. No notes play, or unexpected behavior occurs in the host MIDI application. Timestamps might be incorrect. Review Packet Structure: Carefully check Header Byte (Bit 7=1, Bit 6=0 for non-SysEx, plus 6 MSB of timestamp) and Timestamp Low Byte (Bit 7=1, plus 7 LSB of timestamp). Ensure 0x80 is ORed correctly.
Validate Timestamps: Ensure 13-bit millisecond timestamps are calculated correctly and wrap around 8192ms.
Check MIDI Bytes: Verify the actual MIDI message bytes (Status, Data1, Data2) are correct for the intended musical event.
Log Sent Packets: Log the exact byte array being sent from the ESP32 for debugging.
Notifications Not Enabled or Handled ESP32 appears to connect, but the host MIDI application doesn’t receive any MIDI messages. Serial logs on ESP32 might show attempts to send, but nothing arrives at the host. CCCD Implementation: Ensure a Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902) is added to the MIDI Data I/O characteristic with Read/Write permissions.
Check CCCD Write: In ESP_GATTS_WRITE_EVT, verify the host writes 0x0001 to the CCCD handle to enable notifications. Set a flag accordingly.
Conditional Sending: Only call esp_ble_gatts_send_indicate() (or notify) if this flag indicates notifications are enabled.
Host BLE MIDI Compatibility/Setup ESP32 device is visible in system Bluetooth settings but not within the music software (DAW, app). Connection might fail from within the music app, or MIDI data is not routed correctly. Host Support: Confirm your host OS (macOS, iOS, Windows, Android) and specific music software version support BLE MIDI.
Windows Issues: Windows often requires third-party utilities (e.g., MIDIberry, Korg BLE-MIDI Driver) or DAWs with native BLE MIDI support (e.g., Cakewalk, newer Cubase/Ableton).
DAW Configuration: Check MIDI input settings within the DAW to ensure the ESP32 BLE MIDI device is selected and enabled as an input source.
Restart Host Bluetooth: Sometimes toggling Bluetooth on the host or restarting the host device can resolve connection issues.
MTU Size and SysEx Messages Short MIDI messages (Note On/Off) work, but longer System Exclusive (SysEx) messages are truncated, corrupted, or fail to send/receive completely. Negotiate MTU: The effective MTU is the minimum of client and server. Use esp_ble_gatt_set_local_mtu() to request a larger MTU if needed. Check ESP_GATTS_MTU_EVT.
SysEx Fragmentation: Implement the BLE MIDI SysEx fragmentation scheme. SysEx messages longer than (MTU – 5 bytes for BLE MIDI headers) must be split across multiple BLE packets with appropriate header bits.
Buffer Sizes: Ensure internal buffers on ESP32 are large enough for the SysEx messages you intend to handle.
Timestamp Issues MIDI notes have incorrect timing, jitter, or seem out of sync. Notes might bunch together or have noticeable delays. Accurate Timestamp Source: Use a reliable millisecond timer on the ESP32. esp_log_timestamp() (ms since boot) is a common source. Calculate relative to connection: (esp_log_timestamp() – conn_start_time_ms) & 0x1FFF.
Timestamp Rollover: Be aware the 13-bit timestamp rolls over every 8.192 seconds. Hosts should handle this.
Processing Delays: Minimize processing time on the ESP32 between detecting a musical event and sending the BLE MIDI packet.

Exercises

  1. Button-Controlled MIDI Note:
    • Connect a push button to a GPIO pin on your ESP32.
    • Modify the example so that pressing the button sends a MIDI Note On message, and releasing it sends a MIDI Note Off message for a specific note.
    • Debounce the button input to prevent multiple triggers.
  2. MIDI Control Change Sender:
    • Connect a potentiometer (analog input) to an ADC pin on the ESP32.
    • Read the potentiometer value and map it to a 0-127 range.
    • Implement functionality to send MIDI Control Change (CC) messages (e.g., CC#1 for Modulation Wheel, or CC#7 for Volume) with the mapped potentiometer value. Send the CC message when the value changes significantly.
  3. Receiving and Reacting to MIDI Notes:
    • In the ESP_GATTS_WRITE_EVT handler, parse incoming BLE MIDI packets from the host.
    • If a Note On message for a specific note (e.g., C4) is received, turn on the ESP32’s built-in LED (if available) or an external LED.
    • If a Note Off message for that note is received, turn the LED off.

Summary

  • BLE MIDI allows wireless transmission of MIDI data using custom 128-bit UUIDs for its service (03B80E5A-...) and characteristic (7772E5DB-...).
  • MIDI messages are encapsulated in BLE packets with a header byte (containing timestamp high bits) and a timestamp low byte, followed by the MIDI data. Timestamps are 13-bit millisecond values.
  • The MIDI Data I/O characteristic typically uses WRITE WITHOUT RESPONSE for host-to-peripheral messages and NOTIFY for peripheral-to-host messages.
  • The ESP32 can be effectively used as a BLE MIDI peripheral, sending and receiving MIDI messages to/from DAWs and mobile music applications.
  • Proper packet formatting, UUID usage, and handling of BLE GATT operations (especially notifications) are key to successful implementation.

Further Reading

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top