From 62d1e430d5c0cdc854d859b6641a99d8134c77fc Mon Sep 17 00:00:00 2001 From: Ashley Strahle Date: Wed, 1 Jul 2026 06:34:36 +0000 Subject: [PATCH] Switch from NimBLE-Arduino to ESP32-BLE-MIDI library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct NimBLE-Arduino dependency with max22/ESP32-BLE-MIDI wrapper library (v0.3.2). Rewrite ble_midi_transport to use BLEMidiServer API instead of raw NimBLEDevice calls. Removes patch_nimble_device() from pre_build.py — no longer needed since BT controller init is managed by the library internally. The library uses NimBLE v1.4.1 internally and configures advertising without setScanResponse() or setName(), which may resolve the invisible BLE advertising issue on ESP32-S3. --- include/ble_midi_transport.h | 10 +- platformio.ini | 2 +- pre_build.py | 97 +-------------- src/ble_midi_transport.cpp | 234 ++++++++--------------------------- 4 files changed, 67 insertions(+), 276 deletions(-) diff --git a/include/ble_midi_transport.h b/include/ble_midi_transport.h index 847635e..105ca63 100644 --- a/include/ble_midi_transport.h +++ b/include/ble_midi_transport.h @@ -20,11 +20,13 @@ public: bool is_connected(); - void send_midi_packet(const uint8_t* data, uint8_t len); - void on_receive(const uint8_t* data, size_t len); - size_t parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event); - private: + static void on_control_change(uint8_t channel, uint8_t controller, uint8_t value, uint16_t timestamp); + static void on_note_on(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp); + static void on_note_off(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp); + static void on_connect(); + static void on_disconnect(); + std::function receive_callback; bool initialized; bool client_connected; diff --git a/platformio.ini b/platformio.ini index f21db17..a9fe930 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,7 +8,7 @@ framework = arduino lib_deps = adafruit/Adafruit TinyUSB Library@3.1.0 fastled/FastLED@^3.9.0 - h2zero/NimBLE-Arduino@^1.4.0 + max22/ESP32-BLE-MIDI@^0.3.2 build_unflags = -DARDUINO_USB_MODE=1 diff --git a/pre_build.py b/pre_build.py index 327f000..01b6bdf 100644 --- a/pre_build.py +++ b/pre_build.py @@ -70,8 +70,10 @@ def patch_sdkconfig_bt(): content = f.read() defines = { - "CONFIG_BT_ENABLED": 1, - "CONFIG_BT_NIMBLE_ENABLED": 1, + # Do NOT set CONFIG_BT_ENABLED here — forces Arduino framework + # to pre-init BT controller at startup, which conflicts with + # TinyUSB on ESP32-S3 (causes reboot on USB MIDI send). + # NimBLEDevice::init() handles its own init. "CONFIG_BTDM_CTRL_MODE_BLE_ONLY": 1, "CONFIG_BT_NIMBLE_MAX_CONNECTIONS": 1, "CONFIG_BT_NIMBLE_TASK_STACK_SIZE": 6144, @@ -160,95 +162,6 @@ def patch_nimconfig(): print("nimconfig.h patched successfully") -def patch_nimble_device(): - # Find NimBLEDevice.cpp in the NimBLE-Arduino library - search_dirs = [ - os.path.join(os.getcwd(), ".pio", "libdeps"), - os.path.expanduser("~/.platformio/lib"), - ] - for env_dir in ["esp32s3", ""]: - candidate = os.path.join( - os.getcwd(), ".pio", "libdeps", - env_dir, "NimBLE-Arduino", "src", "NimBLEDevice.cpp" - ) if env_dir else "" - if candidate and os.path.exists(candidate): - dev_path = candidate - break - else: - dev_path = None - for base in search_dirs: - if not os.path.exists(base): - continue - for root, _dirs, files in os.walk(base): - if "NimBLEDevice.cpp" in files: - dev_path = os.path.join(root, "NimBLEDevice.cpp") - break - if dev_path: - break - - if not dev_path: - print("WARNING: NimBLEDevice.cpp not found, skipping controller init patch") - return - - print(f"Patching {dev_path} to handle double BT controller init") - - with open(dev_path, 'r') as f: - content = f.read() - - # Replace the ESP_ERROR_CHECK(esp_bt_controller_init) block to handle - # ESP_ERR_INVALID_STATE (already initialized by Arduino framework when CONFIG_BT_ENABLED) - old_block = ( - ' ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));\n' - ' ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));\n' - ' ESP_ERROR_CHECK(esp_nimble_hci_init());' - ) - new_block = ( - ' if (esp_bt_controller_init(&bt_cfg) == ESP_OK) {\n' - ' ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));\n' - ' }\n' - ' ESP_ERROR_CHECK(esp_nimble_hci_init());' - ) - - if old_block in content: - content = content.replace(old_block, new_block) - with open(dev_path, 'w') as f: - f.write(content) - print(" Patched esp_bt_controller_init to skip if already initialized") - else: - # Try alternative indentation (1 tab = 8 spaces) - old_block_alt = old_block.replace(' ', '\t') - new_block_alt = new_block.replace(' ', '\t') - if old_block_alt in content: - content = content.replace(old_block_alt, new_block_alt) - with open(dev_path, 'w') as f: - f.write(content) - print(" Patched (tab-indented) esp_bt_controller_init") - else: - # Try with partial indentation - patterns = [ - (r'(\s*)ESP_ERROR_CHECK\(esp_bt_controller_init\(&bt_cfg\)\);\s*\n' - r'\1ESP_ERROR_CHECK\(esp_bt_controller_enable\(ESP_BT_MODE_BLE\)\);\s*\n' - r'\1ESP_ERROR_CHECK\(esp_nimble_hci_init\(\)\);'), - ] - for pattern in patterns: - replacement = ( - r'\1if (esp_bt_controller_init(&bt_cfg) == ESP_OK) {\n' - r'\1 ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));\n' - r'\1}\n' - r'\1ESP_ERROR_CHECK(esp_nimble_hci_init());' - ) - new_content = re.sub(pattern, replacement, content) - if new_content != content: - content = new_content - with open(dev_path, 'w') as f: - f.write(content) - print(" Patched esp_bt_controller_init (regex)") - break - else: - print(" WARNING: Could not find esp_bt_controller_init pattern in NimBLEDevice.cpp") - - patch_usb_ids() patch_sdkconfig_bt() -patch_nimconfig() -patch_nimble_device() \ No newline at end of file +patch_nimconfig() \ No newline at end of file diff --git a/src/ble_midi_transport.cpp b/src/ble_midi_transport.cpp index cd6559c..9db54f8 100644 --- a/src/ble_midi_transport.cpp +++ b/src/ble_midi_transport.cpp @@ -1,38 +1,9 @@ #include "ble_midi_transport.h" #include -#include - -#define BLE_MIDI_SERVICE_UUID "03B80E5A-EDE8-4B33-A751-6CE34EC4C700" -#define BLE_MIDI_CHAR_UUID "7772E5DB-3868-4112-A1A9-F2669D106BF3" +#include static BleMidiTransport* instance = nullptr; -static NimBLEServer* ble_server = nullptr; -static NimBLEService* ble_service = nullptr; -static NimBLECharacteristic* ble_char = nullptr; -static bool device_connected = false; - -class ServerCallbacks : public NimBLEServerCallbacks { - void onConnect(NimBLEServer* server) override { - device_connected = true; - Serial.println("[BLE] Client connected"); - } - void onDisconnect(NimBLEServer* server) override { - device_connected = false; - Serial.println("[BLE] Client disconnected, restarting advertising"); - NimBLEDevice::startAdvertising(); - } -}; - -class CharCallbacks : public NimBLECharacteristicCallbacks { - void onWrite(NimBLECharacteristic* characteristic) override { - std::string value = characteristic->getValue(); - if (value.length() > 0 && instance) { - instance->on_receive((const uint8_t*)value.data(), value.length()); - } - } -}; - BleMidiTransport::BleMidiTransport() : initialized(false), client_connected(false) { instance = this; } @@ -41,179 +12,84 @@ BleMidiTransport::~BleMidiTransport() { if (instance == this) instance = nullptr; } +void BleMidiTransport::on_connect() { + Serial.println("[BLE] Client connected"); + if (instance) instance->client_connected = true; +} + +void BleMidiTransport::on_disconnect() { + Serial.println("[BLE] Client disconnected"); + if (instance) instance->client_connected = false; +} + +void BleMidiTransport::on_control_change(uint8_t channel, uint8_t controller, uint8_t value, uint16_t timestamp) { + if (!instance || !instance->receive_callback) return; + MidiEvent event; + event.type = MidiEvent::CONTROL_CHANGE; + event.channel = channel; + event.data1 = controller; + event.data2 = value; + event.timestamp = timestamp; + instance->receive_callback(event); +} + +void BleMidiTransport::on_note_on(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp) { + if (!instance || !instance->receive_callback) return; + MidiEvent event; + event.type = MidiEvent::NOTE_ON; + event.channel = channel; + event.data1 = note; + event.data2 = velocity; + event.timestamp = timestamp; + instance->receive_callback(event); +} + +void BleMidiTransport::on_note_off(uint8_t channel, uint8_t note, uint8_t velocity, uint16_t timestamp) { + if (!instance || !instance->receive_callback) return; + MidiEvent event; + event.type = MidiEvent::NOTE_OFF; + event.channel = channel; + event.data1 = note; + event.data2 = velocity; + event.timestamp = timestamp; + instance->receive_callback(event); +} + bool BleMidiTransport::begin() { Serial.println("[BLE] Initializing BLE MIDI..."); - - Serial.println("[BLE] NimBLEDevice init..."); - NimBLEDevice::init("JOC Midi"); - Serial.println("[BLE] NimBLEDevice initialized"); - - Serial.println("[BLE] Creating server..."); - ble_server = NimBLEDevice::createServer(); - if (!ble_server) { - Serial.println("[BLE] FAILED to create server"); - return false; - } - Serial.println("[BLE] Server created, setting callbacks..."); - ble_server->setCallbacks(new ServerCallbacks()); - - Serial.println("[BLE] Creating service..."); - ble_service = ble_server->createService(BLE_MIDI_SERVICE_UUID); - if (!ble_service) { - Serial.println("[BLE] FAILED to create service"); - return false; - } - - Serial.println("[BLE] Creating characteristic..."); - Serial.flush(); - ble_char = ble_service->createCharacteristic( - BLE_MIDI_CHAR_UUID, - NIMBLE_PROPERTY::READ | - NIMBLE_PROPERTY::WRITE_NR | - NIMBLE_PROPERTY::NOTIFY - ); - Serial.println("[BLE] characteristic pointer ok"); - if (!ble_char) { - Serial.println("[BLE] FAILED to create characteristic"); - return false; - } - // 0x2902 (CCCD) is auto-created by NimBLE stack when characteristic - // has NOTIFY or INDICATE property - do NOT manually create it. - Serial.println("[BLE] Setting callbacks..."); - ble_char->setCallbacks(new CharCallbacks()); - Serial.println("[BLE] Callbacks set"); - - Serial.println("[BLE] Starting service..."); - ble_service->start(); - Serial.println("[BLE] Service started"); - - Serial.println("[BLE] Starting advertising..."); - NimBLEAdvertising* adv = NimBLEDevice::getAdvertising(); - adv->setName("JOC Midi"); - adv->addServiceUUID(BLE_MIDI_SERVICE_UUID); - adv->setScanResponse(true); - adv->start(); - - initialized = true; + BLEMidiServer.begin("JOC Midi"); + BLEMidiServer.setOnConnectCallback(on_connect); + BLEMidiServer.setOnDisconnectCallback(on_disconnect); + BLEMidiServer.setControlChangeCallback(on_control_change); + BLEMidiServer.setNoteOnCallback(on_note_on); + BLEMidiServer.setNoteOffCallback(on_note_off); Serial.println("[BLE] BLE MIDI advertising as 'JOC Midi'"); + initialized = true; return true; } void BleMidiTransport::update() { - client_connected = device_connected; + client_connected = BLEMidiServer.isConnected(); } void BleMidiTransport::on_midi_receive(std::function callback) { receive_callback = callback; } -void BleMidiTransport::send_midi_packet(const uint8_t* data, uint8_t len) { - if (!initialized || !client_connected || !ble_char) return; - - uint16_t timestamp = micros() & 0x3FFF; - uint8_t packet[16]; - uint8_t idx = 0; - packet[idx++] = 0x80 | (timestamp >> 7); - packet[idx++] = timestamp & 0x7F; - for (uint8_t i = 0; i < len && idx < 16; i++) { - packet[idx++] = data[i]; - } - - ble_char->setValue(packet, idx); - ble_char->notify(); -} - void BleMidiTransport::send_note_on(uint8_t channel, uint8_t note, uint8_t velocity) { - uint8_t packet[3] = {(uint8_t)(0x90 | (channel - 1)), note, velocity}; - send_midi_packet(packet, 3); + BLEMidiServer.noteOn(channel, note, velocity); Serial.printf("[BLE OUT] Ch:%d NOTE_ON:%d:%d\n", channel, note, velocity); } void BleMidiTransport::send_note_off(uint8_t channel, uint8_t note, uint8_t velocity) { - uint8_t packet[3] = {(uint8_t)(0x80 | (channel - 1)), note, velocity}; - send_midi_packet(packet, 3); + BLEMidiServer.noteOff(channel, note, velocity); Serial.printf("[BLE OUT] Ch:%d NOTE_OFF:%d:%d\n", channel, note, velocity); } void BleMidiTransport::send_cc(uint8_t channel, uint8_t cc, uint8_t value) { - uint8_t packet[3] = {(uint8_t)(0xB0 | (channel - 1)), cc, value}; - send_midi_packet(packet, 3); + BLEMidiServer.controlChange(channel, cc, value); } bool BleMidiTransport::is_connected() { return initialized && client_connected; } - -void BleMidiTransport::on_receive(const uint8_t* data, size_t len) { - if (len < 3 || !receive_callback) return; // need timestamp(2) + status(1) - - // BLE MIDI format: [ts_hi|0x80] [ts_lo] [status ...] - size_t offset = 2; // skip timestamp header - - while (offset < len) { - uint8_t status = data[offset++]; - if (status < 0x80) continue; // skip non-status bytes - - uint8_t type = status & 0xF0; - uint8_t channel = (status & 0x0F) + 1; - - MidiEvent event; - event.channel = channel; - event.timestamp = millis(); - event.type = MidiEvent::NOTE_ON; // default - - offset = parse_ble_midi(status, data, len, offset, event); - - if (event.type == MidiEvent::SYSEX) { - Serial.printf("[BLE IN] Sysex len=%zu\n", len); - } else { - Serial.printf("[BLE IN] Ch:%d %s:%d:%d\n", - event.channel, - event.type == MidiEvent::NOTE_ON ? "NOTE_ON" : - event.type == MidiEvent::NOTE_OFF ? "NOTE_OFF" : - event.type == MidiEvent::CONTROL_CHANGE ? "CC" : "OTHER", - event.data1, event.data2); - } - - if (receive_callback) { - receive_callback(event); - } - } -} - -size_t BleMidiTransport::parse_ble_midi(uint8_t status, const uint8_t* data, size_t len, size_t offset, MidiEvent& event) { - uint8_t type = status & 0xF0; - event.data1 = 0; - event.data2 = 0; - - if (offset >= len) return offset; - - event.data1 = data[offset++]; - - switch (type) { - case 0x80: - event.type = MidiEvent::NOTE_OFF; - if (offset < len) event.data2 = data[offset++]; - break; - case 0x90: - event.type = MidiEvent::NOTE_ON; - if (offset < len) event.data2 = data[offset++]; - if (event.data2 == 0) event.type = MidiEvent::NOTE_OFF; - break; - case 0xB0: - event.type = MidiEvent::CONTROL_CHANGE; - if (offset < len) event.data2 = data[offset++]; - break; - case 0xC0: - event.type = MidiEvent::PROGRAM_CHANGE; - break; - case 0xE0: - event.type = MidiEvent::PITCH_BEND; - if (offset < len) event.data2 = data[offset++]; - break; - default: - break; - } - - return offset; -}