diff --git a/examples/MidiBle_Client/MidiBle_Client.ino b/examples/MidiBle_Client/MidiBle_Client.ino new file mode 100644 index 0000000..9a44bdf --- /dev/null +++ b/examples/MidiBle_Client/MidiBle_Client.ino @@ -0,0 +1,141 @@ +/** + * -------------------------------------------------------- + * This example shows how to use client MidiBLE + * Client BLEMIDI works im a similar way Server (Common) BLEMIDI, but with some exception. + * + * The most importart exception is read() method. This function works as usual, but + * now it manages machine-states BLE connection too. The + * read() function must be called several times continuously in order to scan BLE device + * and connect with the server. In this example, read() is called in a "multitask function of + * FreeRTOS", but it can be called in loop() function as usual. + * + * Some BLEMIDI_CREATE_INSTANCE() are added in MidiBLE-Client to be able to choose a specific server to connect + * or to connect to the first server which has the MIDI characteristic. You can choose the server by typing in the name field + * the name of the server or the BLE address of the server. If you want to connect + * to the first MIDI server BLE found by the device, you just have to set the name field empty (""). + * + * FOR ADVANCED USERS: Other advanced BLE configurations can be changed in hardware/BLEMIDI_Client_ESP32.h + * #defines in the head of the file (IMPORTANT: Only the first user defines must be modified). These configurations + * are related to security (password, pairing and securityCallback()), communication params, the device name + * and other stuffs. Modify defines at your own risk. + * + * + * + * @auth RobertoHE + * -------------------------------------------------------- + */ + +#include +#include + +#include + +//#include +//#include +//#include +//#include + +BLEMIDI_CREATE_DEFAULT_INSTANCE(); //Connect to first server found + +//BLEMIDI_CREATE_INSTANCE("",MIDI) //Connect to the first server found +//BLEMIDI_CREATE_INSTANCE("f2:c1:d9:36:e7:6b",MIDI) //Connect to a specific BLE address server +//BLEMIDI_CREATE_INSTANCE("MyBLEserver",MIDI) //Connect to a specific name server + +#ifndef LED_BUILTIN +#define LED_BUILTIN 2 //modify for match with yout board +#endif + +void ReadCB(void *parameter); //Continuos Read function (See FreeRTOS multitasks) + +unsigned long t0 = millis(); +bool isConnected = false; + +/** + * ----------------------------------------------------------------------------- + * When BLE is connected, LED will turn on (indicating that connection was successful) + * When receiving a NoteOn, LED will go out, on NoteOff, light comes back on. + * This is an easy and conveniant way to show that the connection is alive and working. + * ----------------------------------------------------------------------------- +*/ +void setup() +{ + Serial.begin(115200); + MIDI.begin(MIDI_CHANNEL_OMNI); + + BLEMIDI.setHandleConnected([]() + { + Serial.println("---------CONNECTED---------"); + isConnected = true; + digitalWrite(LED_BUILTIN, HIGH); + }); + + BLEMIDI.setHandleDisconnected([]() + { + Serial.println("---------NOT CONNECTED---------"); + isConnected = false; + digitalWrite(LED_BUILTIN, LOW); + }); + + MIDI.setHandleNoteOn([](byte channel, byte note, byte velocity) + { + Serial.print("NoteON: CH: "); + Serial.print(channel); + Serial.print(" | "); + Serial.print(note); + Serial.print(", "); + Serial.println(velocity); + digitalWrite(LED_BUILTIN, LOW); + }); + MIDI.setHandleNoteOff([](byte channel, byte note, byte velocity) + { + digitalWrite(LED_BUILTIN, HIGH); + }); + + xTaskCreatePinnedToCore(ReadCB, //See FreeRTOS for more multitask info + "MIDI-READ", + 3000, + NULL, + 1, + NULL, + 1); //Core0 or Core1 + + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LOW); +} + +// ----------------------------------------------------------------------------- +// +// ----------------------------------------------------------------------------- +void loop() +{ + //MIDI.read(); // This function is called in the other task + + if (isConnected && (millis() - t0) > 1000) + { + t0 = millis(); + + MIDI.sendNoteOn(60, 100, 1); // note 60, velocity 100 on channel 1 + vTaskDelay(250/portTICK_PERIOD_MS); + MIDI.sendNoteOff(60, 0, 1); + } +} + +/** + * This function is called by xTaskCreatePinnedToCore() to perform a multitask execution. + * In this task, read() is called every millisecond (approx.). + * read() function performs connection, reconnection and scan-BLE functions. + * Call read() method repeatedly to perform a successfull connection with the server + * in case connection is lost. +*/ +void ReadCB(void *parameter) +{ +// Serial.print("READ Task is started on core: "); +// Serial.println(xPortGetCoreID()); + for (;;) + { + MIDI.read(); + vTaskDelay(1 / portTICK_PERIOD_MS); //Feed the watchdog of FreeRTOS. + //Serial.println(uxTaskGetStackHighWaterMark(NULL)); //Only for debug. You can see the watermark of the free resources assigned by the xTaskCreatePinnedToCore() function. + } + vTaskDelay(1); +} diff --git a/src/BLEMIDI_Transport.h b/src/BLEMIDI_Transport.h index b960e6f..a759813 100644 --- a/src/BLEMIDI_Transport.h +++ b/src/BLEMIDI_Transport.h @@ -12,7 +12,7 @@ BEGIN_BLEMIDI_NAMESPACE -template +template class BLEMIDI_Transport { typedef _Settings Settings; @@ -23,31 +23,36 @@ private: byte mTxBuffer[Settings::MaxBufferSize]; // minimum 5 bytes unsigned mTxIndex = 0; - + char mDeviceName[24]; uint8_t mTimestampLow; private: - T mBleClass; + T mBleClass; -public: - BLEMIDI_Transport(const char* deviceName) - { +public: + BLEMIDI_Transport(const char *deviceName) + { strncpy(mDeviceName, deviceName, sizeof(mDeviceName)); - + mRxIndex = 0; mTxIndex = 0; - } + } -public: +public: static const bool thruActivated = false; - + void begin() { mBleClass.begin(mDeviceName, this); } + void end() + { + mBleClass.end(); + } + void end() { mBleClass.end(); @@ -58,10 +63,10 @@ public: getMidiTimestamp(&mTxBuffer[0], &mTxBuffer[1]); mTxIndex = 2; mTimestampLow = mTxBuffer[1]; // or generate new ? - + return true; } - + void write(byte inData) { if (mTxIndex >= sizeof(mTxBuffer)) @@ -81,8 +86,8 @@ public: { mBleClass.write(mTxBuffer, mTxIndex - 1); - mTxIndex = 1; // keep header - mTxBuffer[mTxIndex++] = mTimestampLow; // or generate new ? + mTxIndex = 1; // keep header + mTxBuffer[mTxIndex++] = mTimestampLow; // or generate new ? } else { @@ -94,7 +99,7 @@ public: mBleClass.write(mTxBuffer, mTxIndex); mTxIndex = 0; } - + byte read() { return mRxBuffer[--mRxIndex]; @@ -104,13 +109,13 @@ public: { uint8_t byte; auto success = mBleClass.available(&byte); - if (!success) return mRxIndex; + if (!success) + return mRxIndex; mRxBuffer[mRxIndex++] = byte; - return mRxIndex; } - + protected: /* The first byte of all BLE packets must be a header byte. This is followed by timestamp bytes and MIDI messages. @@ -122,7 +127,6 @@ protected: The header byte contains the topmost 6 bits of timing information for MIDI events in the BLE packet. The remaining 7 bits of timing information for individual MIDI messages encoded in a packet is expressed by timestamp bytes. - Timestamp Byte bit 7 Set to 1. bits 6-0 timestampLow: Least Significant 7 bits of timestamp information. @@ -157,31 +161,36 @@ protected: and the MSB of both bytes are set to indicate that this is a header byte. Both bytes are placed into the first two position of an array in preparation for a MIDI message. */ - static void getMidiTimestamp (uint8_t *header, uint8_t *timestamp) + static void getMidiTimestamp(uint8_t *header, uint8_t *timestamp) { auto currentTimeStamp = millis() & 0x01FFF; - - *header = ((currentTimeStamp >> 7) & 0x3F) | 0x80; // 6 bits plus MSB - *timestamp = (currentTimeStamp & 0x7F) | 0x80; // 7 bits plus MSB + + *header = ((currentTimeStamp >> 7) & 0x3F) | 0x80; // 6 bits plus MSB + *timestamp = (currentTimeStamp & 0x7F) | 0x80; // 7 bits plus MSB } - - static void setMidiTimestamp (uint8_t header, uint8_t *timestamp) + + static uint16_t setMidiTimestamp(uint8_t header, uint8_t timestamp) { + auto timestampHigh = 0x3f & header; + auto timestampLow = 0x7f & timestamp; + return (timestampLow + (timestampHigh << 7)); } - -public: - // callbacks - void(*_connectedCallback)() = nullptr; - void(*_disconnectedCallback)() = nullptr; public: - void setHandleConnected(void(*fptr)()) { - _connectedCallback = fptr; - } + // callbacks + void (*_connectedCallback)() = nullptr; + void (*_disconnectedCallback)() = nullptr; - void setHandleDisconnected(void(*fptr)()) { - _disconnectedCallback = fptr; - } +public: + void setHandleConnected(void (*fptr)()) + { + _connectedCallback = fptr; + } + + void setHandleDisconnected(void (*fptr)()) + { + _disconnectedCallback = fptr; + } /* The general form of a MIDI message follows: @@ -213,83 +222,182 @@ public: 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(byte* buffer, size_t length) - { + + /** + * If #define RUNNING_ENABLE is commented, it will transform all incoming runningStatus messages in full midi messages. + * Else, it will put in the buffer the same info that it had received (runningStatus will be not transformated). + * It recommend not use runningStatus by default. Only use if parser accepts runningStatus and your application has a so high transmission rate. + */ + //#define RUNNING_ENABLE + + void receive(byte *buffer, size_t length) + { // Pointers used to search through payload. - byte lPtr = 0; - byte rPtr = 0; + int lPtr = 0; + int rPtr = 0; + // lastStatus used to capture runningStatus byte lastStatus; - // Decode first packet -- SHALL be "Full MIDI message" - lPtr = 2; //Start at first MIDI status -- SHALL be "MIDI status" - + // previousStatus used to continue a runningStatus interrupted by a timeStamp or a System Message. + byte previousStatus = 0x00; + + byte headerByte = buffer[lPtr++]; + + auto timestampHigh = 0x3f & headerByte; + + byte timestampByte = buffer[lPtr++]; + uint16_t timestamp = 0; + + bool sysExContinuation = false; + bool runningStatusContinuation = false; + + if (timestampByte >= 80) // if bit 7 is 1, it's a timestampByte + { + auto timestampLow = 0x7f & timestampByte; + timestamp = timestampLow + (timestampHigh << 7); + } + else // if bit 7 is 0, it's the Continuation of a previous SysEx + { + sysExContinuation = true; + lPtr--; // the second byte is part of the SysEx + } + //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 + + if (previousStatus == 0x00) + { + if ((lastStatus < 0x80) && !sysExContinuation) + return; // Status message not present and it is not a runningStatus continuation, bail + } + else if (lastStatus < 0x80) + { + lastStatus = previousStatus; + runningStatusContinuation = true; + } // Point to next non-data byte rPtr = lPtr; - while( (buffer[rPtr + 1] < 0x80) && (rPtr < (length - 1)) ) + 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 - mBleClass.add(lastStatus); - } else if( rPtr - lPtr < 2 ) { - mBleClass.add(lastStatus); - mBleClass.add(buffer[lPtr + 1]); - } else if( rPtr - lPtr < 3 ) { - mBleClass.add(lastStatus); - mBleClass.add(buffer[lPtr + 1]); - mBleClass.add(buffer[lPtr + 2]); - } else { - // Too much data + if (!runningStatusContinuation) + { // If not System Common or System Real-Time, send it as running status - switch(buffer[lPtr] & 0xF0) + + auto midiType = lastStatus & 0xF0; + if (sysExContinuation) + midiType = 0xF0; + + switch (midiType) { case 0x80: case 0x90: case 0xA0: case 0xB0: case 0xE0: +#ifdef RUNNING_ENABLE + mBleClass.add(lastStatus); +#endif for (auto i = lPtr; i < rPtr; i = i + 2) { +#ifndef RUNNING_ENABLE mBleClass.add(lastStatus); +#endif mBleClass.add(buffer[i + 1]); mBleClass.add(buffer[i + 2]); } break; case 0xC0: case 0xD0: +#ifdef RUNNING_ENABLE + mBleClass.add(lastStatus); +#endif for (auto i = lPtr; i < rPtr; i = i + 1) { +#ifndef RUNNING_ENABLE mBleClass.add(lastStatus); +#endif mBleClass.add(buffer[i + 1]); } break; case 0xF0: - mBleClass.add(buffer[lPtr]); + mBleClass.add(lastStatus); for (auto i = lPtr; i < rPtr; i++) mBleClass.add(buffer[i + 1]); + break; + default: break; } } - + else + { +#ifndef RUNNING_ENABLE + auto midiType = lastStatus & 0xF0; + if (sysExContinuation) + midiType = 0xF0; + + switch (midiType) + { + case 0x80: + case 0x90: + case 0xA0: + case 0xB0: + case 0xE0: + //3 bytes full Midi -> 2 bytes runningStatus + for (auto i = lPtr; i <= rPtr; i = i + 2) + { + mBleClass.add(lastStatus); + mBleClass.add(buffer[i]); + mBleClass.add(buffer[i + 1]); + } + break; + case 0xC0: + case 0xD0: + //2 bytes full Midi -> 1 byte runningStatus + for (auto i = lPtr; i <= rPtr; i = i + 1) + { + mBleClass.add(lastStatus); + mBleClass.add(buffer[i]); + } + break; + + default: + break; + } +#else + mBleClass.add(lastStatus); + for (auto i = lPtr; i <= rPtr; i++) + mBleClass.add(buffer[i]); +#endif + runningStatusContinuation = false; + } + + if (++rPtr >= length) + return; // end of packet + + if (lastStatus < 0xf0) //exclude System Message. They must not be RunningStatus + { + previousStatus = lastStatus; + } + + timestampByte = buffer[rPtr++]; + if (timestampByte >= 80) // is bit 7 set? + { + auto timestampLow = 0x7f & timestampByte; + timestamp = timestampLow + (timestampHigh << 7); + } + // Point to next status - lPtr = rPtr + 2; - if(lPtr >= length) + lPtr = rPtr; + if (lPtr >= length) return; //end of packet } - } - + } }; struct MySettings : public MIDI_NAMESPACE::DefaultSettings @@ -298,4 +406,3 @@ struct MySettings : public MIDI_NAMESPACE::DefaultSettings }; END_BLEMIDI_NAMESPACE - diff --git a/src/hardware/BLEMIDI_Client_ESP32.h b/src/hardware/BLEMIDI_Client_ESP32.h new file mode 100644 index 0000000..6b066e3 --- /dev/null +++ b/src/hardware/BLEMIDI_Client_ESP32.h @@ -0,0 +1,572 @@ +#pragma once + +/* +############################################# +########## USER DEFINES BEGINNING ########### +####### Modify only these parameters ######## +############################################# +*/ + +/* +##### BLE DEVICE NAME ##### +*/ + +/** + * Set always the same name independently of name server + */ +//#define BLEMIDI_CLIENT_FIXED_NAME "BleMidiClient" + +#ifndef BLEMIDI_CLIENT_FIXED_NAME //Not modify +/** + * When client tries to connect to specific server, BLE name is composed as follows: + * BLEMIDI_CLIENT_NAME_PREFIX + + BLEMIDI_CLIENT_NAME_SUBFIX + * + * example: + * BLEMIDI_CLIENT_NAME_PREFIX "Client-" + * "AX-Edge" + * BLEMIDI_CLIENT_NAME_SUBFIX "-Midi1" + * + * Result: "Client-AX-Edge-Midi1" + */ +#define BLEMIDI_CLIENT_NAME_PREFIX "C-" +#define BLEMIDI_CLIENT_NAME_SUBFIX "" + +/** + * When client tries to connect to the first midi server found: + */ +#define BLEMIDI_CLIENT_DEFAULT_NAME "BLEMIDI-CLIENT" +#endif //Not modify + +/* +###### SECURITY ##### +*/ + +/** Set the IO capabilities of the device, each option will trigger a different pairing method. + * BLE_HS_IO_KEYBOARD_ONLY - Passkey pairing + * BLE_HS_IO_DISPLAY_YESNO - Numeric comparison pairing + * BLE_HS_IO_NO_INPUT_OUTPUT - DEFAULT setting - just works pairing + */ +#define BLEMIDI_CLIENT_SECURITY_CAP BLE_HS_IO_NO_INPUT_OUTPUT + +/** Set the security method. + * bonding + * man in the middle protection + * pair. secure connections + * + * More info in nimBLE lib + * + * Uncomment what you need + * These are the default values. + */ +//#define BLEMIDI_CLIENT_BOND +//#define BLEMIDI_CLIENT_MITM +#define BLEMIDI_CLIENT_PAIR + +/** + * This callback function defines what will be done when server requieres PassKey. + * Add your custom code here. + */ +static uint32_t userOnPassKeyRequest() +{ + //FILL WITH YOUR CUSTOM AUTH METHOD or PASSKEY + //FOR EXAMPLE: + uint32_t passkey = 123456; + + //Serial.println("Client Passkey Request"); + + /** return the passkey to send to the server */ + return passkey; +}; + +/* +###### BLE COMMUNICATION PARAMS ###### +*/ + /** Set connection parameters: + * If you only use one connection, put recomended BLE server param communication + * (you may scan it ussing "nRF Connect" app or other similar apps). + * + * If you use more than one connection adjust, for example, settings like 15ms interval, 0 latency, 120ms timout. + * These settings may be safe for 3 clients to connect reliably, set faster values if you have less + * connections. + * + * Min interval (unit: 1.25ms): 12 * 1.25ms = 15 ms, + * Max interval (unit: 1.25ms): 12 * 1.25ms = 15, + * 0 latency (Number of intervals allowed to skip), + * TimeOut (unit: 10ms) 51 * 10ms = 510ms. Timeout should be minimum 100ms. + */ +#define BLEMIDI_CLIENT_COMM_MIN_INTERVAL 6 // 7.5ms +#define BLEMIDI_CLIENT_COMM_MAX_INTERVAL 35 // 40ms +#define BLEMIDI_CLIENT_COMM_LATENCY 0 +#define BLEMIDI_CLIENT_COMM_TIMEOUT 200 //2000ms + +/* +############################################# +############ USER DEFINES END ############### +############################################# +*/ + +// Headers for ESP32 nimBLE +#include + +BEGIN_BLEMIDI_NAMESPACE + +#ifdef BLEMIDI_CLIENT_BOND +#define BLEMIDI_CLIENT_BOND_DUMMY BLE_SM_PAIR_AUTHREQ_BOND +#else +#define BLEMIDI_CLIENT_BOND_DUMMY 0x00 +#endif + +#ifdef BLEMIDI_CLIENT_MITM +#define BLEMIDI_CLIENT_MITM_DUMMY BLE_SM_PAIR_AUTHREQ_MITM +#else +#define BLEMIDI_CLIENT_MITM_DUMMY 0x00 +#endif + +#ifdef BLEMIDI_CLIENT_PAIR +#define BLEMIDI_CLIENT_PAIR_DUMMY BLE_SM_PAIR_AUTHREQ_SC +#else +#define BLEMIDI_CLIENT_PAIR_DUMMY 0x00 +#endif + +/** Set the security method. + * bonding + * man in the middle protection + * pair. secure connections + * + * More info in nimBLE lib + */ +#define BLEMIDI_CLIENT_SECURITY_AUTH (BLEMIDI_CLIENT_BOND_DUMMY | BLEMIDI_CLIENT_MITM_DUMMY | BLEMIDI_CLIENT_PAIR_DUMMY) + +/** Define a class to handle the callbacks when advertisments are received */ +class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks +{ +public: + NimBLEAdvertisedDevice advDevice; + bool doConnect = false; + bool scanDone = false; + bool specificTarget = false; + bool enableConnection = false; + std::string nameTarget; + +protected: + void onResult(NimBLEAdvertisedDevice *advertisedDevice) + { + if (enableConnection) //not begin() or end() + { + Serial.print("Advertised Device found: "); + Serial.println(advertisedDevice->toString().c_str()); + if (advertisedDevice->isAdvertisingService(NimBLEUUID(SERVICE_UUID))) + { + Serial.println("Found MIDI Service"); + if (!specificTarget || (advertisedDevice->getName() == nameTarget.c_str() || advertisedDevice->getAddress() == nameTarget)) + { + /** Ready to connect now */ + doConnect = true; + /** Save the device reference in a public variable that the client can use*/ + advDevice = *advertisedDevice; + /** stop scan before connecting */ + NimBLEDevice::getScan()->stop(); + } + else + { + Serial.println("Name error"); + } + } + else + { + doConnect = false; + } + } + }; +}; + +/** Define a funtion to handle the callbacks when scan ends */ +void scanEndedCB(NimBLEScanResults results); + +/** Define the class that performs Client Midi (nimBLE) */ +class BLEMIDI_Client_ESP32 +{ +private: + BLEClient *_client = nullptr; + BLEAdvertising *_advertising = nullptr; + BLERemoteCharacteristic *_characteristic = nullptr; + BLERemoteService *pSvc = nullptr; + + BLEMIDI_Transport *_bleMidiTransport = nullptr; + + bool specificTarget = false; + + friend class AdvertisedDeviceCallbacks; + friend class MyClientCallbacks; + friend class MIDI_NAMESPACE::MidiInterface, MySettings>; // + + AdvertisedDeviceCallbacks myAdvCB; + +protected: + QueueHandle_t mRxQueue; + +public: + BLEMIDI_Client_ESP32() + { + } + + bool begin(const char *, BLEMIDI_Transport *); + + bool end() + { + myAdvCB.enableConnection = false; + xQueueReset(mRxQueue); + _client->disconnect(); + _client = nullptr; + + return !_client->isConnected(); + } + + void write(uint8_t *data, uint8_t length) + { + if (!myAdvCB.enableConnection) + return; + if (_characteristic == NULL) + return; + _characteristic->writeValue(data, length, true); + } + + bool available(byte *pvBuffer); + + void add(byte value) + { + // called from BLE-MIDI, to add it to a buffer here + xQueueSend(mRxQueue, &value, portMAX_DELAY/2); + } + +protected: + void receive(uint8_t *buffer, size_t length) + { + // forward the buffer so that it can be parsed + _bleMidiTransport->receive(buffer, length); + } + + void connectCallbacks(MIDI_NAMESPACE::MidiInterface, MySettings> *MIDIcallback); + + void connected() + { + if (_bleMidiTransport->_connectedCallback) + { + _bleMidiTransport->_connectedCallback(); + } + } + + void disconnected() + { + if (_bleMidiTransport->_disconnectedCallback) + { + _bleMidiTransport->_disconnectedCallback(); + } + } + + void notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify); + + void scan(); + bool connect(); +}; + +/** Define the class that performs interruption callbacks */ +class MyClientCallbacks : public BLEClientCallbacks +{ +public: + MyClientCallbacks(BLEMIDI_Client_ESP32 *bluetoothEsp32) + : _bluetoothEsp32(bluetoothEsp32) + { + } + +protected: + BLEMIDI_Client_ESP32 *_bluetoothEsp32 = nullptr; + + uint32_t onPassKeyRequest() + { + return userOnPassKeyRequest(); + }; + + void onConnect(BLEClient *pClient) + { + //Serial.println("##Connected##"); + pClient->updateConnParams(BLEMIDI_CLIENT_COMM_MIN_INTERVAL, BLEMIDI_CLIENT_COMM_MAX_INTERVAL, BLEMIDI_CLIENT_COMM_LATENCY, BLEMIDI_CLIENT_COMM_TIMEOUT); + vTaskDelay(1); + if (_bluetoothEsp32) + { + _bluetoothEsp32->connected(); + } + }; + + void onDisconnect(BLEClient *pClient) + { + //Serial.print(pClient->getPeerAddress().toString().c_str()); + //Serial.println(" Disconnected - Starting scan"); + + if (_bluetoothEsp32) + { + _bluetoothEsp32->disconnected(); + } + + //Try reconnection or search a new one + NimBLEDevice::getScan()->start(1, scanEndedCB); + } + + bool onConnParamsUpdateRequest(NimBLEClient *pClient, const ble_gap_upd_params *params) + { + if (params->itvl_min < BLEMIDI_CLIENT_COMM_MIN_INTERVAL) + { /** 1.25ms units */ + return false; + } + else if (params->itvl_max > BLEMIDI_CLIENT_COMM_MAX_INTERVAL) + { /** 1.25ms units */ + return false; + } + else if (params->latency > BLEMIDI_CLIENT_COMM_LATENCY) + { /** Number of intervals allowed to skip */ + return false; + } + else if (params->supervision_timeout > BLEMIDI_CLIENT_COMM_TIMEOUT + 10) + { /** 10ms units */ + return false; + } + + return true; + }; +}; + +/* +########################################## +############# IMPLEMENTATION ############# +########################################## +*/ + +bool BLEMIDI_Client_ESP32::begin(const char *deviceName, BLEMIDI_Transport *bleMidiTransport) +{ + _bleMidiTransport = bleMidiTransport; + + std::string strDeviceName(deviceName); + if (strDeviceName == "") // Connect to the first midi server found + { + myAdvCB.specificTarget = false; + myAdvCB.nameTarget = ""; + +#ifdef BLEMIDI_CLIENT_FIXED_NAME + strDeviceName = BLEMIDI_CLIENT_FIXED_NAME; +#else + strDeviceName = BLEMIDI_CLIENT_DEFAULT_NAME; +#endif + } + else // Connect to a specific name or address + { + myAdvCB.specificTarget = true; + myAdvCB.nameTarget = strDeviceName; + +#ifdef BLEMIDI_CLIENT_FIXED_NAME + strDeviceName = BLEMIDI_CLIENT_FIXED_NAME; +#else + strDeviceName = BLEMIDI_CLIENT_NAME_PREFIX + strDeviceName + BLEMIDI_CLIENT_NAME_SUBFIX; +#endif + } + Serial.println(strDeviceName.c_str()); + + NimBLEDevice::init(strDeviceName); + + // To communicate between the 2 cores. + // Core_0 runs here, core_1 runs the BLE stack + mRxQueue = xQueueCreate(256, sizeof(uint8_t)); // TODO Settings::MaxBufferSize + + NimBLEDevice::setSecurityIOCap(BLEMIDI_CLIENT_SECURITY_CAP); // Attention, it may need a passkey + NimBLEDevice::setSecurityAuth(BLEMIDI_CLIENT_SECURITY_AUTH); + + /** Optional: set the transmit power, default is 3db */ + NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */ + + myAdvCB.enableConnection = true; + scan(); + + return true; +} + +bool BLEMIDI_Client_ESP32::available(byte *pvBuffer) +{ + if (myAdvCB.enableConnection) + { + if (_client == nullptr || !_client->isConnected()) //Try to connect/reconnect + { + if (myAdvCB.doConnect) + { + myAdvCB.doConnect = false; + if (!connect()) + { + scan(); + } + } + else if (myAdvCB.scanDone) + { + scan(); + } + } + // return 1 byte from the Queue + return xQueueReceive(mRxQueue, (void *)pvBuffer, 0); // return immediately when the queue is empty + } + else + { + return false; + } +} + +/** Notification receiving handler callback */ +void BLEMIDI_Client_ESP32::notifyCB(NimBLERemoteCharacteristic *pRemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify) +{ + if (this->_characteristic == pRemoteCharacteristic) //Redundant protection + { + receive(pData, length); + } +} + +void BLEMIDI_Client_ESP32::scan() +{ + // Retrieve a Scanner and set the callback you want to use to be informed when a new device is detected. + // Specify that you want active scanning and start the + // scan to run for 3 seconds. + myAdvCB.scanDone = true; + NimBLEScan *pBLEScan = BLEDevice::getScan(); + if (!pBLEScan->isScanning()) + { + pBLEScan->setAdvertisedDeviceCallbacks(&myAdvCB); + pBLEScan->setInterval(600); + pBLEScan->setWindow(500); + pBLEScan->setActiveScan(true); + + Serial.println("Scanning..."); + pBLEScan->start(1, scanEndedCB); + } +}; + +bool BLEMIDI_Client_ESP32::connect() +{ + using namespace std::placeholders; //<- for bind funtion in callback notification + + /** Check if we have a client we should reuse first + * Special case when we already know this device + * This saves considerable time and power. + */ + + if (_client) + { + if (_client == NimBLEDevice::getClientByPeerAddress(myAdvCB.advDevice.getAddress())) + { + if (_client->connect(&myAdvCB.advDevice, false)) + { + if (_characteristic->canNotify()) + { + if (_characteristic->subscribe(true, std::bind(&BLEMIDI_Client_ESP32::notifyCB, this, _1, _2, _3, _4))) + { + //Re-connection SUCCESS + return true; + } + } + /** Disconnect if subscribe failed */ + _client->disconnect(); + } + /* If any connection problem exits, delete previous client and try again in the next attemp as new client*/ + NimBLEDevice::deleteClient(_client); + _client = nullptr; + return false; + } + /*If client does not match, delete previous client and create a new one*/ + NimBLEDevice::deleteClient(_client); + _client = nullptr; + } + + if (NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) + { + Serial.println("Max clients reached - no more connections available"); + return false; + } + + // Create and setup a new client + _client = BLEDevice::createClient(); + + _client->setClientCallbacks(new MyClientCallbacks(this), false); + + _client->setConnectionParams(BLEMIDI_CLIENT_COMM_MIN_INTERVAL, BLEMIDI_CLIENT_COMM_MAX_INTERVAL, BLEMIDI_CLIENT_COMM_LATENCY, BLEMIDI_CLIENT_COMM_TIMEOUT); + + /** Set how long we are willing to wait for the connection to complete (seconds), default is 30. */ + _client->setConnectTimeout(15); + + if (!_client->connect(&myAdvCB.advDevice)) + { + /** Created a client but failed to connect, don't need to keep it as it has no data */ + NimBLEDevice::deleteClient(_client); + _client = nullptr; + //Serial.println("Failed to connect, deleted client"); + return false; + } + + if (!_client->isConnected()) + { + //Serial.println("Failed to connect"); + _client->disconnect(); + NimBLEDevice::deleteClient(_client); + _client = nullptr; + return false; + } + + Serial.print("Connected to: "); + Serial.print(myAdvCB.advDevice.getName().c_str()); + Serial.print(" / "); + Serial.println(_client->getPeerAddress().toString().c_str()); + + /* + Serial.print("RSSI: "); + Serial.println(_client->getRssi()); + */ + + /** Now we can read/write/subscribe the charateristics of the services we are interested in */ + pSvc = _client->getService(SERVICE_UUID); + if (pSvc) /** make sure it's not null */ + { + _characteristic = pSvc->getCharacteristic(CHARACTERISTIC_UUID); + + if (_characteristic) /** make sure it's not null */ + { + if (_characteristic->canNotify()) + { + if (_characteristic->subscribe(true, std::bind(&BLEMIDI_Client_ESP32::notifyCB, this, _1, _2, _3, _4))) + { + //Connection SUCCESS + return true; + } + } + } + } + + //If anything fails, disconnect and delete client + _client->disconnect(); + NimBLEDevice::deleteClient(_client); + _client = nullptr; + return false; +}; + +/** Callback to process the results of the last scan or restart it */ +void scanEndedCB(NimBLEScanResults results) +{ + // Serial.println("Scan Ended"); +} + +END_BLEMIDI_NAMESPACE + +/*! \brief Create an instance for ESP32 named , and adviertise it like "Prefix + + Subfix" + It will try to connect to a specific server with equal name or addr than . If is "", it will connect to first midi server + */ +#define BLEMIDI_CREATE_INSTANCE(DeviceName, Name) \ + BLEMIDI_NAMESPACE::BLEMIDI_Transport BLE##Name(DeviceName); \ + MIDI_NAMESPACE::MidiInterface, BLEMIDI_NAMESPACE::MySettings> Name((BLEMIDI_NAMESPACE::BLEMIDI_Transport &)BLE##Name); + +/*! \brief Create a default instance for ESP32 named BLEMIDI-CLIENT. + It will try to connect to first midi ble server found. + */ +#define BLEMIDI_CREATE_DEFAULT_INSTANCE() \ + BLEMIDI_CREATE_INSTANCE("", MIDI)