Merge pull request #30 from RobertoHE/master

Client for BLEMIDI
This commit is contained in:
lathoub 2021-08-06 09:11:31 +02:00 committed by GitHub
commit f1e0982e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 889 additions and 69 deletions

View File

@ -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 <Arduino.h>
#include <BLEMIDI_Transport.h>
#include <hardware/BLEMIDI_Client_ESP32.h>
//#include <hardware/BLEMIDI_ESP32_NimBLE.h>
//#include <hardware/BLEMIDI_ESP32.h>
//#include <hardware/BLEMIDI_nRF52.h>
//#include <hardware/BLEMIDI_ArduinoBLE.h>
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);
}

View File

@ -12,7 +12,7 @@
BEGIN_BLEMIDI_NAMESPACE BEGIN_BLEMIDI_NAMESPACE
template<class T, class _Settings = DefaultSettings> template <class T, class _Settings = DefaultSettings>
class BLEMIDI_Transport class BLEMIDI_Transport
{ {
typedef _Settings Settings; typedef _Settings Settings;
@ -32,7 +32,7 @@ private:
T mBleClass; T mBleClass;
public: public:
BLEMIDI_Transport(const char* deviceName) BLEMIDI_Transport(const char *deviceName)
{ {
strncpy(mDeviceName, deviceName, sizeof(mDeviceName)); strncpy(mDeviceName, deviceName, sizeof(mDeviceName));
@ -53,6 +53,11 @@ public:
mBleClass.end(); mBleClass.end();
} }
void end()
{
mBleClass.end();
}
bool beginTransmission(MIDI_NAMESPACE::MidiType type) bool beginTransmission(MIDI_NAMESPACE::MidiType type)
{ {
getMidiTimestamp(&mTxBuffer[0], &mTxBuffer[1]); getMidiTimestamp(&mTxBuffer[0], &mTxBuffer[1]);
@ -104,10 +109,10 @@ public:
{ {
uint8_t byte; uint8_t byte;
auto success = mBleClass.available(&byte); auto success = mBleClass.available(&byte);
if (!success) return mRxIndex; if (!success)
return mRxIndex;
mRxBuffer[mRxIndex++] = byte; mRxBuffer[mRxIndex++] = byte;
return mRxIndex; return mRxIndex;
} }
@ -122,7 +127,6 @@ protected:
The header byte contains the topmost 6 bits of timing information for MIDI events in the BLE 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. The remaining 7 bits of timing information for individual MIDI messages encoded in a
packet is expressed by timestamp bytes. packet is expressed by timestamp bytes.
Timestamp Byte Timestamp Byte
bit 7 Set to 1. bit 7 Set to 1.
bits 6-0 timestampLow: Least Significant 7 bits of timestamp information. bits 6-0 timestampLow: Least Significant 7 bits of timestamp information.
@ -157,7 +161,7 @@ protected:
and the MSB of both bytes are set to indicate that this is a header byte. 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. 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; auto currentTimeStamp = millis() & 0x01FFF;
@ -165,21 +169,26 @@ protected:
*timestamp = (currentTimeStamp & 0x7F) | 0x80; // 7 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: public:
// callbacks // callbacks
void(*_connectedCallback)() = nullptr; void (*_connectedCallback)() = nullptr;
void(*_disconnectedCallback)() = nullptr; void (*_disconnectedCallback)() = nullptr;
public: public:
void setHandleConnected(void(*fptr)()) { void setHandleConnected(void (*fptr)())
{
_connectedCallback = fptr; _connectedCallback = fptr;
} }
void setHandleDisconnected(void(*fptr)()) { void setHandleDisconnected(void (*fptr)())
{
_disconnectedCallback = fptr; _disconnectedCallback = fptr;
} }
@ -213,83 +222,182 @@ public:
MIDI messages. In the MIDI BLE protocol, the System Real-Time messages must be deinterleaved MIDI messages. In the MIDI BLE protocol, the System Real-Time messages must be deinterleaved
from other messages except for System Exclusive messages. 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. // Pointers used to search through payload.
byte lPtr = 0; int lPtr = 0;
byte rPtr = 0; int rPtr = 0;
// lastStatus used to capture runningStatus // lastStatus used to capture runningStatus
byte lastStatus; byte lastStatus;
// Decode first packet -- SHALL be "Full MIDI message" // previousStatus used to continue a runningStatus interrupted by a timeStamp or a System Message.
lPtr = 2; //Start at first MIDI status -- SHALL be "MIDI status" 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 statement contains incrementing pointers and breaks when buffer size exceeded.
while (true) while (true)
{ {
lastStatus = buffer[lPtr]; lastStatus = buffer[lPtr];
if( (buffer[lPtr] < 0x80)) if (previousStatus == 0x00)
return; // Status message not present, bail {
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 // Point to next non-data byte
rPtr = lPtr; rPtr = lPtr;
while( (buffer[rPtr + 1] < 0x80) && (rPtr < (length - 1)) ) while ((buffer[rPtr + 1] < 0x80) && (rPtr < (length - 1)))
rPtr++; rPtr++;
if (buffer[rPtr + 1] == 0xF7) rPtr++;
// look at l and r pointers and decode by size. if (!runningStatusContinuation)
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 not System Common or System Real-Time, send it as running status // 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 0x80:
case 0x90: case 0x90:
case 0xA0: case 0xA0:
case 0xB0: case 0xB0:
case 0xE0: case 0xE0:
#ifdef RUNNING_ENABLE
mBleClass.add(lastStatus);
#endif
for (auto i = lPtr; i < rPtr; i = i + 2) for (auto i = lPtr; i < rPtr; i = i + 2)
{ {
#ifndef RUNNING_ENABLE
mBleClass.add(lastStatus); mBleClass.add(lastStatus);
#endif
mBleClass.add(buffer[i + 1]); mBleClass.add(buffer[i + 1]);
mBleClass.add(buffer[i + 2]); mBleClass.add(buffer[i + 2]);
} }
break; break;
case 0xC0: case 0xC0:
case 0xD0: case 0xD0:
#ifdef RUNNING_ENABLE
mBleClass.add(lastStatus);
#endif
for (auto i = lPtr; i < rPtr; i = i + 1) for (auto i = lPtr; i < rPtr; i = i + 1)
{ {
#ifndef RUNNING_ENABLE
mBleClass.add(lastStatus); mBleClass.add(lastStatus);
#endif
mBleClass.add(buffer[i + 1]); mBleClass.add(buffer[i + 1]);
} }
break; break;
case 0xF0: case 0xF0:
mBleClass.add(buffer[lPtr]); mBleClass.add(lastStatus);
for (auto i = lPtr; i < rPtr; i++) for (auto i = lPtr; i < rPtr; i++)
mBleClass.add(buffer[i + 1]); mBleClass.add(buffer[i + 1]);
break; break;
default: default:
break; 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 // Point to next status
lPtr = rPtr + 2; lPtr = rPtr;
if(lPtr >= length) if (lPtr >= length)
return; //end of packet return; //end of packet
} }
} }
}; };
struct MySettings : public MIDI_NAMESPACE::DefaultSettings struct MySettings : public MIDI_NAMESPACE::DefaultSettings
@ -298,4 +406,3 @@ struct MySettings : public MIDI_NAMESPACE::DefaultSettings
}; };
END_BLEMIDI_NAMESPACE END_BLEMIDI_NAMESPACE

View File

@ -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 + <NameServer/addrServer> + BLEMIDI_CLIENT_NAME_SUBFIX
*
* example:
* BLEMIDI_CLIENT_NAME_PREFIX "Client-"
* <NameServer/addrServer> "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 <NimBLEDevice.h>
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<class BLEMIDI_Client_ESP32> *_bleMidiTransport = nullptr;
bool specificTarget = false;
friend class AdvertisedDeviceCallbacks;
friend class MyClientCallbacks;
friend class MIDI_NAMESPACE::MidiInterface<BLEMIDI_Transport<BLEMIDI_Client_ESP32>, MySettings>; //
AdvertisedDeviceCallbacks myAdvCB;
protected:
QueueHandle_t mRxQueue;
public:
BLEMIDI_Client_ESP32()
{
}
bool begin(const char *, BLEMIDI_Transport<class BLEMIDI_Client_ESP32> *);
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<BLEMIDI_Transport<BLEMIDI_Client_ESP32>, 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<class BLEMIDI_Client_ESP32> *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 <DeviceName>, and adviertise it like "Prefix + <DeviceName> + Subfix"
It will try to connect to a specific server with equal name or addr than <DeviceName>. If <DeviceName> is "", it will connect to first midi server
*/
#define BLEMIDI_CREATE_INSTANCE(DeviceName, Name) \
BLEMIDI_NAMESPACE::BLEMIDI_Transport<BLEMIDI_NAMESPACE::BLEMIDI_Client_ESP32> BLE##Name(DeviceName); \
MIDI_NAMESPACE::MidiInterface<BLEMIDI_NAMESPACE::BLEMIDI_Transport<BLEMIDI_NAMESPACE::BLEMIDI_Client_ESP32>, BLEMIDI_NAMESPACE::MySettings> Name((BLEMIDI_NAMESPACE::BLEMIDI_Transport<BLEMIDI_NAMESPACE::BLEMIDI_Client_ESP32> &)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)