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; -}