iosarduinobluetooth-lowenergymidinrf52

How to keep an active MIDI BLE connection between an Arduino NANO 33 BLE (nRF52) and an iPad


I'm realizing a BLE MIDI controller for iPad using an Arduino Nano 33 BLE device. The following code is able to:

The connection is stable only with Android apps. Each iOS app (eg Garageband, AUM, etc.) closes the connection immediately (the led on the arduino board turns on and off in few seconds), but if the device constantly sends MIDI messages (look at the lines of code commented in the loop() function) the connection remains active forever; unfortunately the repeated sending of messages is not the purpose of the controller that I want to realize.

There are probably specific configurations of the BLE service or polling actions to be implemented to comply with the strict iOS standards, but I could not find any working solution or example for the Nano 33 BLE device that does not include sending notes in the loop() function.

#include <ArduinoBLE.h>

byte midiData[] = {0x80, 0x80, 0x00, 0x00, 0x00};

// set up the MIDI service and MIDI message characteristic:
BLEService midiService("03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
BLECharacteristic midiCharacteristic("7772E5DB-3868-4112-A1A9-F2669D106BF3",
                                     BLEWrite | BLEWriteWithoutResponse |
                                     BLENotify | BLERead, sizeof(midiData));
bool midi_connected = false;

void setup() {
  // initialize serial communication
  Serial.begin(9600);
  // initialize built in LED:
  pinMode(LED_BUILTIN, OUTPUT);
  // Initialize BLE service:
  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");
    while (true);
  }
  BLE.setLocalName("MBLE");
  BLE.setAdvertisedService(midiService);
  BLE.setEventHandler(BLEConnected, onConnected);
  BLE.setEventHandler(BLEDisconnected, onDisconnected);
  midiCharacteristic.setEventHandler(BLEWritten, onWritten);
  midiService.addCharacteristic(midiCharacteristic);
  BLE.addService(midiService);

  BLE.setConnectable(true);
  BLE.setAdvertisingInterval(32);
  BLE.setConnectionInterval(32, 64);
  BLE.advertise();
}

void loop() {
  BLEDevice central = BLE.central();
  if (central) {
//    midiCommand(0x90, 60, 127);
//    delay(250);
//    midiCommand(0x80, 60, 0);
//    delay(250);
  }
}

void onConnected(BLEDevice central) {
  digitalWrite(LED_BUILTIN, HIGH);
  midi_connected = true;
}

void onDisconnected(BLEDevice central) {
  digitalWrite(LED_BUILTIN, LOW);
  midi_connected = false;
}

void onWritten(BLEDevice central, BLECharacteristic characteristic) {
  auto buffer = characteristic.value();
  auto length = characteristic.valueLength();

  if (length > 0)
  {
    // echo on the next midi channel
    midiCommand(buffer[2], buffer[3], buffer[4]);
  }
}

void midiCommand(byte cmd, byte data1, byte  data2) {
  midiData[2] = cmd;
  midiData[3] = data1;
  midiData[4] = data2;
  midiCharacteristic.setValue(midiData, sizeof(midiData));
}

Solution

  • I (finally) found a solution by myself looking at the MIDI BLE Specifications provided by Apple that says

    The accessory shall request a connection interval of 15 ms or less. Apple recommends starting with a request for a connection interval of 11.25 ms and going to 15 ms if the connection request is rejected by the Apple product. Intervals higher than 15 ms are unsuitable for live playback situations.

    and later

    Apple devices that support Bluetooth Low Energy MIDI will attempt to read the MIDI I/O characteristic after establishing a connection with the accessory. [...] The accessory shall respond to the initial MIDI I/O characteristic read with a packet that has no payload.

    So I changed the connection interval in the setup() function

    BLE.setConnectionInterval(9, 12);
    

    and included few lines in connection event handler function

    void onConnected(BLEDevice central) {
      digitalWrite(LED_BUILTIN, HIGH);
      midi_connected = true;
      midiCharacteristic.setValue(0);
    }
    

    That's it!