diff --git a/include/ble_midi_transport.h b/include/ble_midi_transport.h index eedd1c4..847635e 100644 --- a/include/ble_midi_transport.h +++ b/include/ble_midi_transport.h @@ -20,21 +20,12 @@ public: bool is_connected(); - void queue_cc(uint8_t channel, uint8_t cc, uint8_t value); + 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; - - uint8_t queued_channel; - uint8_t queued_cc; - uint8_t queued_value; - bool queued; }; diff --git a/platformio.ini b/platformio.ini index 23a202c..41477e8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,8 +8,7 @@ framework = arduino lib_deps = adafruit/Adafruit TinyUSB Library@3.1.0 fastled/FastLED@^3.9.0 - max22/ESP32-BLE-MIDI@^0.3.2 - h2zero/NimBLE-Arduino@^1.4.3 + h2zero/NimBLE-Arduino@^1.4.0 build_unflags = -DARDUINO_USB_MODE=1 @@ -19,6 +18,7 @@ build_flags = -DARDUINO_USB_CDC_ON_BOOT=1 -DUSE_TINYUSB=1 -Wno-macro-redefined + -DMYNEWT_VAL_BLE_EXT_ADV=0 monitor_speed = 115200 diff --git a/pre_build.py b/pre_build.py index 70783d7..4c2cb16 100644 --- a/pre_build.py +++ b/pre_build.py @@ -184,29 +184,29 @@ def patch_nimble_device(): with open(dev_path, 'r') as f: content = f.read() - # v1.4.3 code has ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); # Arduino framework inits controller but does NOT enable it. - # We must enable even if init returns INVALID_STATE (already init'd). - 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 = ( - 'esp_err_t __bt_err = esp_bt_controller_init(&bt_cfg);\n' - ' if (__bt_err == ESP_OK || __bt_err == ESP_ERR_INVALID_STATE) {\n' - ' ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));\n' - ' }\n' - ' ESP_ERROR_CHECK(esp_nimble_hci_init());' - ) + # Handle both init outcomes: skip if already initialized, but always try to enable. + patterns = [ + (r'(\s*)ESP_ERROR_CHECK\(esp_bt_controller_init\(&bt_cfg\)\);\s*' + r'\1ESP_ERROR_CHECK\(esp_bt_controller_enable\(ESP_BT_MODE_BLE\)\);\s*' + r'\1ESP_ERROR_CHECK\(esp_nimble_hci_init\(\)\);', + r'\1esp_err_t __bt_err = esp_bt_controller_init(&bt_cfg);\n' + r'\1if (__bt_err == ESP_OK || __bt_err == ESP_ERR_INVALID_STATE) {\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());') + ] - 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: - print(" WARNING: Could not find init pattern in NimBLEDevice.cpp (may already be patched)") + for pattern, replacement in patterns: + 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 to skip if already initialized") + return + + print(" WARNING: Could not find init pattern in NimBLEDevice.cpp") patch_usb_ids() diff --git a/src/app_task.cpp b/src/app_task.cpp index 937a9be..cc3d152 100644 --- a/src/app_task.cpp +++ b/src/app_task.cpp @@ -54,7 +54,7 @@ void AppTask::update() { pending_cc = false; delay(1); usb_midi->send_cc(pending_channel, pending_cc_num, pending_value); - if (ble_midi) ble_midi->queue_cc(pending_channel, pending_cc_num, pending_value); + if (ble_midi) ble_midi->send_cc(pending_channel, pending_cc_num, pending_value); } } diff --git a/src/ble_midi_transport.cpp b/src/ble_midi_transport.cpp index 7b8d515..f6112b2 100644 --- a/src/ble_midi_transport.cpp +++ b/src/ble_midi_transport.cpp @@ -1,10 +1,39 @@ #include "ble_midi_transport.h" #include -#include +#include + +#define BLE_MIDI_SERVICE_UUID "03B80E5A-EDE8-4B33-A751-6CE34EC4C700" +#define BLE_MIDI_CHAR_UUID "7772E5DB-3868-4112-A1A9-F2669D106BF3" static BleMidiTransport* instance = nullptr; -BleMidiTransport::BleMidiTransport() : initialized(false), client_connected(false), queued(false) { +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; } @@ -12,94 +41,188 @@ 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..."); - 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'"); + + // NimBLE-Arduino v1.4.0+ handles ESP32-S3 internally, using + // bt_cfg.bluetooth_mode = ESP_BT_MODE_BLE. Do NOT pre-init the + // BT controller here or NimBLEDevice::init() will see an + // error from double-init and abort via ESP_ERROR_CHECK. + 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->addServiceUUID(BLE_MIDI_SERVICE_UUID); + adv->setScanResponse(true); + adv->start(); + + if (adv->isAdvertising()) { + Serial.println("[BLE] Advertising confirmed started"); + } else { + Serial.println("[BLE] WARNING: isAdvertising() reports false!"); + } + initialized = true; + Serial.println("[BLE] BLE MIDI advertising as 'JOC Midi'"); return true; } void BleMidiTransport::update() { - client_connected = BLEMidiServer.isConnected(); - - if (queued) { - queued = false; - BLEMidiServer.controlChange(queued_channel, queued_cc, queued_value); - } + client_connected = device_connected; } void BleMidiTransport::on_midi_receive(std::function callback) { receive_callback = callback; } -void BleMidiTransport::queue_cc(uint8_t channel, uint8_t cc, uint8_t value) { - queued_channel = channel; - queued_cc = cc; - queued_value = value; - queued = true; +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) { - queue_cc(channel, note, velocity); + uint8_t packet[3] = {(uint8_t)(0x90 | (channel - 1)), note, velocity}; + send_midi_packet(packet, 3); + 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) { - queue_cc(channel, note, velocity); + uint8_t packet[3] = {(uint8_t)(0x80 | (channel - 1)), note, velocity}; + send_midi_packet(packet, 3); + 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) { - queue_cc(channel, cc, value); + uint8_t packet[3] = {(uint8_t)(0xB0 | (channel - 1)), cc, value}; + send_midi_packet(packet, 3); } 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; +} diff --git a/src/main.cpp b/src/main.cpp index 6f2a6dc..4aee710 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,20 +23,12 @@ AppTask controller(&led_driver, &switch_driver, &midi_transport, &ble_midi_trans TaskHandle_t midi_task_handle = NULL; -void ble_init_task(void* parameter) { - Serial.println("[TASK] BLE init task started on core 0"); - ble_midi_transport.begin(); - Serial.println("[TASK] BLE init complete, deleting task"); - vTaskDelete(NULL); -} - void midi_task(void* parameter) { Serial.println("[TASK] MIDI task started on core 0"); while (true) { midi_transport.update(); ble_midi_transport.update(); - controller.update(); vTaskDelay(1); } } @@ -354,23 +346,16 @@ void setup() { switch_driver.set_mux(&mux); switch_driver.begin(); + Serial.println("[INIT] Initializing BLE MIDI..."); + delay(1000); + ble_midi_transport.begin(); + Serial.println("[INIT] Initializing USB MIDI..."); midi_transport.begin(); delay(500); Serial.println("[INIT] Registering MIDI callbacks..."); controller.begin(); - - Serial.println("[INIT] Starting BLE init on core 0..."); - xTaskCreatePinnedToCore( - ble_init_task, - "ble_init", - 8192, - NULL, - 3, - NULL, - 0 - ); xTaskCreatePinnedToCore( midi_task, @@ -391,6 +376,7 @@ void setup() { void loop() { led_driver.update(); + controller.update(); if (Serial.available()) { String cmd = Serial.readStringUntil('\n');