ESP32 BLE Midi: How to send data from client to server

hoschi
Posts: 4
Joined: Wed Jan 26, 2022 4:21 pm

ESP32 BLE Midi: How to send data from client to server

Postby hoschi » Thu Mar 17, 2022 9:14 pm

Hi all

A while ago I wrote a sketch providing Midi over BLE. It allows me to connect apps on my iPhone to get paired with my ESP32. I can send Midi commands back and forth. It works with all apps I tried so far. I assume it is more or less based on common example we all can find in the internet. I did put it into a class - here's my code:

Code: Select all

#include "BleMidi.h"

extern midi::MidiInterface<HardwareSerial> midiA;
extern Settings settings;
extern BleMidi *pBleMidi;
extern uint8_t midiPacket[];

BLEServer *pServer = NULL;
            
class BleMidiServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      Serial.println("Connected to client");
      BleMidi::deviceConnected = true;

      Serial.println(">> Readvertising");
      pServer->getAdvertising()->start();
    };

    void onDisconnect(BLEServer* pServer) {
      Serial.println("Disconneted from client");
      BleMidi::deviceConnected = false;
    }
};

class BleMidiCharacteristicCallbacks: public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic *pCharacteristic) {
      
      std::string rxValue = pCharacteristic->getValue();

      pBleMidi->pCharacteristic->setValue((uint8_t*)pCharacteristic->getValue().c_str(), rxValue.length()); // packet, length in bytes
      pBleMidi->pCharacteristic->notify();


      uint8_t midiMessageLength = rxValue.length();
      uint8_t processedBytes = 0;

      uint8_t type, note, velocity, channel, d1, d2;
      
      if (midiMessageLength < 3) {
        Serial.println("Received Midi Message too short. Aborting!");
        return;
      }

      Serial.println("*********");
      Serial.print("Received Value: ");
      for (int i = 0; i < rxValue.length(); i++) {
        Serial.print(rxValue[i], HEX);
        Serial.print(" "); 
      }
  
      Serial.println();
      Serial.println("*********");

      // timestamp #1 and timestamp #2
      processedBytes += 2;
      
      while(processedBytes < midiMessageLength) {
        
        channel = rxValue[processedBytes] % 16;
        type = rxValue[processedBytes++] - channel;

        switch(type) {
          case midi::ControlChange:
            note = rxValue[processedBytes++]; 
            velocity = rxValue[processedBytes++];
            midiA.sendControlChange(note, velocity, channel+1);
            Serial.println(String("Control Change: controller=") + note + ", value=" + velocity);
            break;
          case midi::NoteOn:
            note = rxValue[processedBytes++]; 
            velocity = rxValue[processedBytes++]; 
            channel = rxValue[processedBytes++];
            midiA.sendNoteOn(note, velocity, channel+1);
            if (velocity > 0) {
              Serial.println(String("Note On:  ch=") + channel + ", note=" + note + ", velocity=" + velocity);
            } else {
              Serial.println(String("Note Off: ch=") + channel + ", note=" + note);
            }
            break;
          case midi::NoteOff:
            note = rxValue[processedBytes++]; 
            velocity = rxValue[processedBytes++]; 
            channel = rxValue[processedBytes++];
            midiA.sendNoteOff(note, velocity, channel+1);
            Serial.println(String("Note Off: ch=") + channel + ", note=" + note + ", velocity=" + velocity);
            break;
          case midi::ProgramChange:
            note = rxValue[processedBytes++];
            midiA.sendProgramChange(note, channel+1);
            Serial.println(String("Program Change: ch=") + channel + String(", program=") + note);
            break;
          case midi::SystemExclusive: {
            uint8_t *sysexData = new uint8_t[midiMessageLength];
            uint8_t byteCnt = 0;
            int iF0 = -1;
            int iF7 = -1;
            uint8_t timingTag = 0x80;
            
            for(int i=0 ; i<midiMessageLength ; i++) {
              if(rxValue[i] == 0xF0) {
                if(iF0 != -1)
                  Serial.println("Already found a F0 - should not happen");
                iF0 = i;
                if(i > 0)
                  timingTag = rxValue[i-1];
              }
              else if(rxValue[i] == 0xF7) {
                if(iF7 != -1)
                  Serial.println("Already found a F7 - should not happen");
                iF7 = i;
              }

              if(iF0 != -1 && iF7 != -1) {
                if(iF7 < iF0)
                  Serial.println("iF7 is smaller than iF0 -> should never happen");
                else {
                  for(int j=iF0; ; j++) {
                    if(rxValue[j] == timingTag)
                      continue;
                    sysexData[byteCnt++] = rxValue[j];
                    if(rxValue[j] == 0xF7)
                      break;
                  }

                  
                  if(byteCnt > 3) {
                    if(sysexData[0] == 0xF0 && sysexData[1] == 0x33 && sysexData[2] == 0x20 && sysexData[3] == 0x00) {
                      settings.processMidi(sysexData, byteCnt, pCharacteristic);
                    }
                    else {
                      midiA.sendSysEx(byteCnt, sysexData, true);
                      Serial.print("Data to Physical Midi Out: ");
                      for (int i = 0; i < byteCnt ; i++) {
                        Serial.print(sysexData[i], HEX); Serial.print(" ");
                      }
                      Serial.println();
                    }
                  }
                  iF0 = iF7 = -1;
                  byteCnt = 0;
                  timingTag = 0x80;
                }
              }
            }
            delete sysexData;
            return;
            break;
          }
          default:
            break;
          
        }
        // skip one byte as we expected the next timestamp
        processedBytes++;
      } 
    }
};

bool BleMidi::deviceConnected = false;

BleMidi::BleMidi() {
  Serial.print("Initialising BLE device... ");
  char name[17];
  strcpy(name, settings.name); name[16] = '\0';
  
  BLEDevice::init(name);
  Serial.print("My address: "); Serial.println(BLEDevice::getAddress().toString().c_str());
  // Create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new BleMidiServerCallbacks());
  BLEDevice::setEncryptionLevel((esp_ble_sec_act_t)ESP_LE_AUTH_REQ_SC_BOND);
  Serial.println("Done!");

  Serial.print("Creating MIDI BLE Service... ");
  // Create the BLE Service
  BLEService *pService = pServer->createService(BLEUUID(MIDI_SERVICE_UUID));
  
  // Create a BLE Characteristic
  pCharacteristic = pService->createCharacteristic(
                      BLEUUID(MIDI_CHARACTERISTIC_UUID),
                      BLECharacteristic::PROPERTY_READ   |
                      BLECharacteristic::PROPERTY_NOTIFY |
                      BLECharacteristic::PROPERTY_WRITE_NR
                    );
  pCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
  Serial.println("Done!");
  
  // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.descriptor.gatt.client_characteristic_configuration.xml
  // Create a BLE Descriptor
  pCharacteristic->addDescriptor(new BLE2902());
  BleMidiCharacteristicCallbacks *pBleMidiCallbacks = new BleMidiCharacteristicCallbacks();
  pCharacteristic->setCallbacks(pBleMidiCallbacks);
  Serial.print("Starting BLE service... ");
  pService->start();
  Serial.println("Done!");
  
  Serial.print("Starting advertisement... ");
  BLESecurity *pSecurity = new BLESecurity();
  pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
  pSecurity->setCapability(ESP_IO_CAP_NONE);
  pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
  
  pServer->getAdvertising()->addServiceUUID(MIDI_SERVICE_UUID);
  
  pServer->getAdvertising()->start();
  
  Serial.println("Done!");
  
  Serial.print("BLE service is running - device discoverable as: "); Serial.println(settings.name);
}
Now I want to connect a "client" to the server shown above. Based on the BLE_client example (which comes with the ESP32 BLE Arduino package) I came up with the following sketch:

Code: Select all

/**
 * A BLE client example that is rich in capabilities.
 * There is a lot new capabilities implemented.
 * author unknown
 * updated by chegewara
 */

#include "BLEDevice.h"
//#include "BLEScan.h"

// The remote service we wish to connect to.
static BLEUUID serviceUUID("03b80e5a-ede8-4b33-a751-6ce34ec4c700");
// The characteristic of the remote service we are interested in.
static BLEUUID    charUUID("7772e5db-3868-4112-a1a9-f2669d106bf3");

static boolean doConnect = false;
static boolean connected = false;
static boolean doScan = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;
static BLEAdvertisedDevice* myDevice;

static void notifyCallback(
  BLERemoteCharacteristic* pBLERemoteCharacteristic,
  uint8_t* pData,
  size_t length,
  bool isNotify) {
    Serial.print("Notify callback for characteristic ");
    Serial.print(pBLERemoteCharacteristic->getUUID().toString().c_str());
    Serial.print(" of data length ");
    Serial.println(length);
    Serial.print("data: ");
    Serial.println((char*)pData);
}

class MyClientCallback : public BLEClientCallbacks {
  void onConnect(BLEClient* pclient) {
  }

  void onDisconnect(BLEClient* pclient) {
    connected = false;
    Serial.println("onDisconnect");
  }
};

bool connectToServer() {
    Serial.print("Forming a connection to ");
    Serial.println(myDevice->getAddress().toString().c_str());
    
    BLEClient*  pClient  = BLEDevice::createClient();
    Serial.println(" - Created client");

    pClient->setClientCallbacks(new MyClientCallback());

    // Connect to the remove BLE Server.
    pClient->connect(myDevice);  // if you pass BLEAdvertisedDevice instead of address, it will be recognized type of peer device address (public or private)
    Serial.println(" - Connected to server");

    // Obtain a reference to the service we are after in the remote BLE server.
    BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
    if (pRemoteService == nullptr) {
      Serial.print("Failed to find our service UUID: ");
      Serial.println(serviceUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our service");


    // Obtain a reference to the characteristic in the service of the remote BLE server.
    pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
    if (pRemoteCharacteristic == nullptr) {
      Serial.print("Failed to find our characteristic UUID: ");
      Serial.println(charUUID.toString().c_str());
      pClient->disconnect();
      return false;
    }
    Serial.println(" - Found our characteristic");

    // Read the value of the characteristic.
    if(pRemoteCharacteristic->canRead()) {
      std::string value = pRemoteCharacteristic->readValue();
      Serial.print("The characteristic value was: ");
      Serial.println(value.c_str());
    }

    if(pRemoteCharacteristic->canNotify())
      pRemoteCharacteristic->registerForNotify(notifyCallback);

    connected = true;
    return true;
}
/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.print("BLE Advertised Device found: ");
    Serial.println(advertisedDevice.toString().c_str());

    // We have found a device, let us now see if it contains the service we are looking for.
    if (advertisedDevice.haveServiceUUID() && advertisedDevice.isAdvertisingService(serviceUUID)) {

      BLEDevice::getScan()->stop();
      myDevice = new BLEAdvertisedDevice(advertisedDevice);
      doConnect = true;
      doScan = true;

    } // Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks


void setup() {
  Serial.begin(115200);
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.


// This is the Arduino main loop function.
void loop() {

  // If the flag "doConnect" is true then we have scanned for and found the desired
  // BLE Server with which we wish to connect.  Now we connect to it.  Once we are 
  // connected we set the connected flag to be true.
  if (doConnect == true) {
    if (connectToServer()) {
      Serial.println("We are now connected to the BLE Server.");
    } else {
      Serial.println("We have failed to connect to the server; there is nothin more we will do.");
    }
    doConnect = false;
  }

  // If we are connected to a peer BLE Server, update the characteristic each time we are reached
  // with the current time since boot.
  if (connected) {
    String newValue = "Time since boot: " + String(millis()/1000);
    Serial.println("Setting new characteristic value to \"" + newValue + "\"");
    
    // Set the characteristic's value to be the array of bytes that is actually a string.
    pRemoteCharacteristic->writeValue(newValue.c_str(), newValue.length());
  }else if(doScan){
    BLEDevice::getScan()->start(0);  // this is just example to start scan after disconnect, most likely there is better way to do it in arduino
  }
  
  delay(1000); // Delay a second between loops.
} // End of loop
If I run both sketches, the client properly establishes a connection with the server and I properly receive data sent from server to client. But I am not able to send data back (client to server). I tried several ways calling BLERemoteCharacteristic->writeValue... but had no luck triggering the server's

Code: Select all

void onWrite(BLECharacteristic *pCharacteristic)
function.

Next, I installed a couple of BLE tools on my mobile, such as nRF Connect. Here, I can also connect to my server and send values.


If I send a ByteArray as shown in the attached picture I can properly invoke my BLE server's

Code: Select all

void onWrite(BLECharacteristic *pCharacteristic)
function.
IMG_6646.jpeg
IMG_6646.jpeg (348.8 KiB) Viewed 6934 times
Thus, I believe my server sketch is okay. Also, I need some help sending values to from my client to the server. Any idea what I am doing wrong here?

Thanks in advance!

hoschi
Posts: 4
Joined: Wed Jan 26, 2022 4:21 pm

Re: ESP32 BLE Midi: How to send data from client to server

Postby hoschi » Fri Mar 18, 2022 10:56 am

Found it - as usual: my bad.
I forgot to add proper encryption level in my client sketch. After adding one line to my setup() routine the communication works as expected - here's the complete client's setup-function.

Code: Select all

void setup() {
  Serial.begin(115200);
  Serial1.begin(31250, SERIAL_8N1, 14, 13); // for physical midi connection 14(RX), 13(TX)
  midiB.turnThruOff(); // Prevents forwarding of incoming messages to midi out port
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");
  BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT);

  // Retrieve a Scanner and set the callback we want to use to be informed when we
  // have detected a new device.  Specify that we want active scanning and start the
  // scan to run for 5 seconds.
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
} // End of setup.
Important new line to make it work:

Code: Select all

BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT);

Who is online

Users browsing this forum: No registered users and 83 guests