diff --git a/examples/MidiBle/MidiBle.ino b/examples/MidiBle/MidiBle.ino index 824ebb7..b1bc86d 100644 --- a/examples/MidiBle/MidiBle.ino +++ b/examples/MidiBle/MidiBle.ino @@ -1,5 +1,6 @@ #include -#include +#include +//#include //#include BLEMIDI_CREATE_DEFAULT_INSTANCE() diff --git a/src/BLE-MIDI.h b/src/BLE-MIDI.h index d14d46a..d7b42ce 100755 --- a/src/BLE-MIDI.h +++ b/src/BLE-MIDI.h @@ -45,10 +45,6 @@ public: void begin() { mBleClass.begin(mDeviceName, this); - - // To communicate between the 2 cores. - // Core_0 runs here, core_1 runs the BLE stack -// mRxQueue = xQueueCreate(Settings::MaxBufferSize, sizeof(uint8_t)); } bool beginTransmission(MidiType) @@ -86,17 +82,14 @@ public: unsigned available() { uint8_t byte; - auto succes = mBleClass.available(&byte); // xQueueReceive(mRxQueue, &byte, 0); // return immediately when the queue is empty + auto succes = mBleClass.available(&byte); if (!succes) return mRxIndex; mRxBuffer[mRxIndex++] = byte; return mRxIndex; } - -public: -// QueueHandle_t mRxQueue; - + protected: /* The first byte of all BLE packets must be a header byte. This is followed by timestamp bytes and MIDI messages. diff --git a/src/hardware/ESP32_NimBLE.h b/src/hardware/ESP32_NimBLE.h new file mode 100644 index 0000000..e7c266d --- /dev/null +++ b/src/hardware/ESP32_NimBLE.h @@ -0,0 +1,248 @@ +#pragma once + +// Headers for ESP32 NimBLE +#include + +BEGIN_BLEMIDI_NAMESPACE + +class BLEMIDI_ESP32 +{ +private: + BLEServer* _server = nullptr; + BLEAdvertising* _advertising = nullptr; + BLECharacteristic* _characteristic = nullptr; + + BLEMIDITransport* _bleMidiTransport = nullptr; + +protected: + QueueHandle_t mRxQueue; + +public: + BLEMIDI_ESP32() + { + } + + bool begin(const char*, BLEMIDITransport*); + + void write(uint8_t* buffer, size_t length) + { + _characteristic->setValue(buffer, length); + _characteristic->notify(); + } + + bool available(void *pvBuffer) + { + return xQueueReceive(mRxQueue, pvBuffer, 0); // return immediately when the queue is empty + } + + /* + The general form of a MIDI message follows: + n-byte MIDI Message + Byte 0 MIDI message Status byte, Bit 7 is Set to 1. + Bytes 1 to n-1 MIDI message Data bytes, if n > 1. Bit 7 is Set to 0 + There are two types of MIDI messages that can appear in a single packet: full MIDI messages and + Running Status MIDI messages. Each is encoded differently. + A full MIDI message is simply the MIDI message with the Status byte included. + A Running Status MIDI message is a MIDI message with the Status byte omitted. Running Status + MIDI messages may only be placed in the data stream if the following criteria are met: + 1. The original MIDI message is 2 bytes or greater and is not a System Common or System + Real-Time message. + 2. The omitted Status byte matches the most recently preceding full MIDI message’s Status + byte within the same BLE packet. + In addition, the following rules apply with respect to Running Status: + 1. A Running Status MIDI message is allowed within the packet after at least one full MIDI + message. + 2. Every MIDI Status byte must be preceded by a timestamp byte. Running Status MIDI + messages may be preceded by a timestamp byte. If a Running Status MIDI message is not + preceded by a timestamp byte, the timestamp byte of the most recently preceding message + in the same packet is used. + 3. System Common and System Real-Time messages do not cancel Running Status if + interspersed between Running Status MIDI messages. However, a timestamp byte must + precede the Running Status MIDI message that follows. + 4. The end of a BLE packet does cancel Running Status. + In the MIDI 1.0 protocol, System Real-Time messages can be sent at any time and may be + inserted anywhere in a MIDI data stream, including between Status and Data bytes of any other + MIDI messages. In the MIDI BLE protocol, the System Real-Time messages must be deinterleaved + from other messages – except for System Exclusive messages. + */ + void receive(uint8_t* buffer, size_t length) + { + // Pointers used to search through payload. + uint8_t lPtr = 0; + uint8_t rPtr = 0; + // lastStatus used to capture runningStatus + uint8_t lastStatus; + // Decode first packet -- SHALL be "Full MIDI message" + lPtr = 2; //Start at first MIDI status -- SHALL be "MIDI status" + + //While statement contains incrementing pointers and breaks when buffer size exceeded. + while (true) + { + lastStatus = buffer[lPtr]; + + if( (buffer[lPtr] < 0x80)) + return; // Status message not present, bail + + // Point to next non-data byte + rPtr = lPtr; + while( (buffer[rPtr + 1] < 0x80) && (rPtr < (length - 1)) ) + rPtr++; + if (buffer[rPtr + 1] == 0xF7) rPtr++; + + // look at l and r pointers and decode by size. + if( rPtr - lPtr < 1 ) { + // Time code or system + xQueueSend(mRxQueue, &lastStatus, portMAX_DELAY); + } else if( rPtr - lPtr < 2 ) { + xQueueSend(mRxQueue, &lastStatus, portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[lPtr + 1], portMAX_DELAY); + } else if( rPtr - lPtr < 3 ) { + xQueueSend(mRxQueue, &lastStatus, portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[lPtr + 1], portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[lPtr + 2], portMAX_DELAY); + } else { + // Too much data + // If not System Common or System Real-Time, send it as running status + switch(buffer[lPtr] & 0xF0) + { + case 0x80: + case 0x90: + case 0xA0: + case 0xB0: + case 0xE0: + for (auto i = lPtr; i < rPtr; i = i + 2) + { + xQueueSend(mRxQueue, &lastStatus, portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[i + 1], portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[i + 2], portMAX_DELAY); + } + break; + case 0xC0: + case 0xD0: + for (auto i = lPtr; i < rPtr; i = i + 1) + { + xQueueSend(mRxQueue, &lastStatus, portMAX_DELAY); + xQueueSend(mRxQueue, &buffer[i + 1], portMAX_DELAY); + } + break; + case 0xF0: + xQueueSend(mRxQueue, &buffer[lPtr], portMAX_DELAY); + for (auto i = lPtr; i < rPtr; i++) + xQueueSend(mRxQueue, &buffer[i + 1], portMAX_DELAY); + break; + default: + break; + } + } + + // Point to next status + lPtr = rPtr + 2; + if(lPtr >= length) + return; //end of packet + } + } + + void connected() + { + if (_bleMidiTransport->_connectedCallback) + _bleMidiTransport->_connectedCallback(); + } + + void disconnected() + { + if (_bleMidiTransport->_disconnectedCallback) + _bleMidiTransport->_disconnectedCallback(); + } +}; + +class MyServerCallbacks: public BLEServerCallbacks { +public: + MyServerCallbacks(BLEMIDI_ESP32* bluetoothEsp32) + : _bluetoothEsp32(bluetoothEsp32) { + } + +protected: + BLEMIDI_ESP32* _bluetoothEsp32 = nullptr; + + void onConnect(BLEServer*) { + if (_bluetoothEsp32) + _bluetoothEsp32->connected(); + }; + + void onDisconnect(BLEServer*) { + if (_bluetoothEsp32) + _bluetoothEsp32->disconnected(); + } +}; + +class MyCharacteristicCallbacks: public BLECharacteristicCallbacks { +public: + MyCharacteristicCallbacks(BLEMIDI_ESP32* bluetoothEsp32) + : _bluetoothEsp32(bluetoothEsp32 ) { + } + +protected: + BLEMIDI_ESP32* _bluetoothEsp32 = nullptr; + + void onWrite(BLECharacteristic * characteristic) { + std::string rxValue = characteristic->getValue(); + if (rxValue.length() > 0) { + _bluetoothEsp32->receive((uint8_t *)(rxValue.c_str()), rxValue.length()); + } + } +}; + +bool BLEMIDI_ESP32::begin(const char* deviceName, BLEMIDITransport* bleMidiTransport) +{ + _bleMidiTransport = bleMidiTransport; + + BLEDevice::init(deviceName); + + // To communicate between the 2 cores. + // Core_0 runs here, core_1 runs the BLE stack + mRxQueue = xQueueCreate(64, sizeof(uint8_t)); // TODO Settings::MaxBufferSize + + _server = BLEDevice::createServer(); + _server->setCallbacks(new MyServerCallbacks(this)); + + // Create the BLE Service + auto service = _server->createService(BLEUUID(SERVICE_UUID)); + + // Create a BLE Characteristic + _characteristic = service->createCharacteristic( + BLEUUID(CHARACTERISTIC_UUID), + NIMBLE_PROPERTY::READ | + NIMBLE_PROPERTY::WRITE | + NIMBLE_PROPERTY::NOTIFY | + NIMBLE_PROPERTY::WRITE_NR + ); + + _characteristic->setCallbacks(new MyCharacteristicCallbacks(this)); + // Start the service + service->start(); + + auto advertisementData = BLEAdvertisementData(); + advertisementData.setFlags(0x04); + advertisementData.setCompleteServices(BLEUUID(SERVICE_UUID)); + advertisementData.setName(deviceName); + + // Start advertising + _advertising = _server->getAdvertising(); + _advertising->setAdvertisementData(advertisementData); + _advertising->start(); + + return true; +} + + /*! \brief Create an instance for ESP32 named + */ +#define BLEMIDI_CREATE_INSTANCE(DeviceName, Name) \ +BLEMIDI_NAMESPACE::BLEMIDITransport BLE##Name(DeviceName); \ +MIDI_NAMESPACE::MidiInterface, MySettings> Name((BLEMIDI_NAMESPACE::BLEMIDITransport &)BLE##Name); + + /*! \brief Create a default instance for ESP32 named BLE-MIDI + */ +#define BLEMIDI_CREATE_DEFAULT_INSTANCE() \ +BLEMIDI_CREATE_INSTANCE("BLE-MIDI", MIDI) + +END_BLEMIDI_NAMESPACE