Boron HID over BLE crashes bluetooth on client

Hello, I just started a project with the boron that I plan to use as a macropad / remote of sorts. As such I wanted to start by creating a keyboard out of the boron using the HID service over BLE. I followed the tutorial from Silicon Labs here. I was able to get my iPhone to detect the boron as an HID, but whenever I try to connect, the Bluetooth on my phone reboots and the boron is disconnected. I tried using another example someone posted on a topic here, but it has the same result. I used the examples in the BLE lab (health thermometer) and they worked so I think it is an issue with how I have configured my services. I tried using the Adafruit Bluefruit library for NRF52’s since they support the same SoC that the boron uses, but ended up chasing a rabbit hole of dependencies that are not available through the Particle IDE. I have attached my code below and would really appreciate it if anyone could shed some light or point me in a direction.

The interrupts are how I wanted to send the keys being pressed as my remote currently only has four buttons that won’t be pressed too frequently.

Thank you so much!

Edit: removed logs from ISRs

/*
 * Project MFISwitch
 * Description:
 * Author:
 * Date:
 */


SYSTEM_THREAD(ENABLED);
SYSTEM_MODE(MANUAL);

SerialLogHandler logHandler(LOG_LEVEL_TRACE);

BleUuid genericAccessService(BLE_SIG_UUID_GENERIC_ACCESS_SVC);
BleUuid humanInterfaceDeviceService(BLE_SIG_UUID_HUMAN_INTERFACE_DEVICE_SVC);
BleUuid batteryLevelService(BLE_SIG_UUID_BATTERY_SVC);
BleUuid deviceInfoService(BLE_SIG_UUID_DEVICE_INFORMATION_SVC);

BleCharacteristic batteryLevelCharacteristic("bat", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A19), batteryLevelService);
BleCharacteristic pnpIDCharacteristic("pnp", BleCharacteristicProperty::READ, BleUuid(0x2A50), deviceInfoService);
BleCharacteristic hidInfoCharacteristic("hidInfo", BleCharacteristicProperty::READ, BleUuid(0x2A4A), humanInterfaceDeviceService);
BleCharacteristic protocolModeCharacteristic("protocol", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4E), humanInterfaceDeviceService);
BleCharacteristic reportMapCharacteristic("reportMap", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4B), humanInterfaceDeviceService);
BleCharacteristic reportCharacteristic("report", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4D), humanInterfaceDeviceService);

volatile int lastPressed = -1;

void sendFirst() {
  lastPressed = 0;
}

void sendSecond() {
  lastPressed = 1;
}

void sendThird() {
  lastPressed = 2;
}

void sendFourth() {
  lastPressed = 3;
}

void configureBLE() {
  BLE.addCharacteristic(batteryLevelCharacteristic);
  BLE.addCharacteristic(pnpIDCharacteristic);
  BLE.addCharacteristic(hidInfoCharacteristic);
  BLE.addCharacteristic(protocolModeCharacteristic);
  BLE.addCharacteristic(reportMapCharacteristic);
  BLE.addCharacteristic(reportCharacteristic);

  BleAdvertisingData advertisementData;
  advertisementData.appendLocalName("MFI Switch");
  advertisementData.appendAppearance((ble_sig_appearance_t) BLE_APPEARANCE_HID_KEYBOARD);
  advertisementData.appendServiceUUID(humanInterfaceDeviceService);
  BLE.advertise(&advertisementData);
}

// setup() runs once, when the device is first turned on.
void setup() {

  pinMode(D0, INPUT_PULLDOWN);
  pinMode(D1, INPUT_PULLDOWN);
  pinMode(D2, INPUT_PULLDOWN);
  pinMode(D3, INPUT_PULLDOWN);

  attachInterrupt(D0, sendFirst, RISING);
  attachInterrupt(D1, sendSecond, RISING);
  attachInterrupt(D2, sendThird, RISING);
  attachInterrupt(D3, sendFourth, RISING);
  
  const uint8_t pnpID[7] = {0x02, 0x06, 0x62, 0x0, 0x1, 0x0, 0x1};
  pnpIDCharacteristic.setValue(pnpID, 7);

  const uint8_t hidInfo[4] = {0x01, 0x11, 0x0, 0x21};
  hidInfoCharacteristic.setValue(hidInfo, 4);

  const uint8_t protocolMode[1] = {0x1};
  protocolModeCharacteristic.setValue(protocolMode, 1);

  const uint8_t reportMap[] = {
        0x05, 0x01 , 	// Usage Page (Generic Desktop)
        0x09, 0x06 , 	// Usage (Keyboard)
        0xa1, 0x01 , 	// Collection (Application)
        0x05, 0x07 , 	// Usage Page (Keyboard)
        0x19, 0xe0 , 	// Usage Minimum (Keyboard LeftControl)
        0x29, 0xe7 , 	// Usage Maximum (Keyboard Right GUI)
        0x15, 0x00 , 	// Logical Minimum (0)
        0x25, 0x01 , 	// Logical Maximum (1)
        0x75, 0x01 , 	// Report Size (1)
        0x95, 0x08 , 	// Report Count (8)
        0x81, 0x02 , 	// Input (Data, Variable, Absolute) Modifier byte
        0x95, 0x01 , 	// Report Count (1)
        0x75, 0x08 , 	// Report Size (8)
        0x81, 0x01 , 	// Input (Constant) Reserved byte
        0x95, 0x06 , 	// Report Count (6)
        0x75, 0x08 , 	// Report Size (8)
        0x15, 0x00 , 	// Logical Minimum (0)
        0x25, 0x65 , 	// Logical Maximum (101)
        0x05, 0x07 , 	// Usage Page (Key Codes)
        0x05, 0x01 , 	// Usage Minimum (Reserved (no event indicated))
        0x05, 0x01 , 	// Usage Maximum (Keyboard Application)
        0x05, 0x01 , 	// Input (Data,Array) Key arrays (6 bytes)

        0xC0        // End Collection (Application)
    };
  reportMapCharacteristic.setValue(reportMap, sizeof(reportMap));

  BLE.on();
  BLE.onConnected([](const particle::BlePeerDevice&) {Log.info("Connecting");});
  configureBLE();

  Log.info("Hello World\n");
}

// loop() runs over and over again, as quickly as it can execute.
uint8_t batteryCharge;

void loop() {
  // The core of your code will likely live here.
  if (BLE.connected()) {
    // uint8_t report[] = {0x1, 0x0, KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F};
    // batteryLevelCharacteristic.setValue(report, sizeof(report));

    // float rawCharge = System.batteryCharge();
    // batteryCharge = rawCharge == -1 ? 100 : rawCharge * 100;
    // batteryLevelCharacteristic.setValue(&batteryCharge, 1);
    // Log.info("Level: %d, Raw: %.1f", batteryCharge, rawCharge);
  }
  // Log.info("Last Pressed: %d, First: %d, Second: %d, Third: %d, Fourth: %d", 
  //           lastPressed, digitalRead(D0), digitalRead(D1), digitalRead(D2), digitalRead(D3));
  delay(500);
}

I don’t think Log.info() should be used inside your ISRs.
In order to log the button presses you may want to put that into your loop()

something like this

volatile int lastPressed = 0; // keep 0 as "no info"
                              // positive as button to be dealt with
                              // negative as button handled

void loop() {
  ...
  if (lastPressed > 0) { 
    Log.info("%d was pressed", lastPressed);
    lastPressed *= -1; // mark as handled 
  }
  ...
}

Thank you! I edited the post to remove the logs from the ISRs

I tried your code (with some minor alterations) on a Windows machine and got this on pairing

Device BTHLEDevice\{00001812-0000-1000-8000-00805f9b34fb}_d9b1bf8632fa\8&29749752&3&001f had a problem starting.

Driver Name: hidbthle.inf
Class Guid: {745a17a0-74d3-11d0-b6fe-00a0c90f57da}
Service: mshidumdf
Lower Filters: WUDFRd
Upper Filters: 
Problem: 0xA
Problem Status: 0xC00000E5

However, with the nRF Connect app I can connect to the device without crashing.

The nRF Connect app and Bluefuit Connect apps both crash for me as well (also on iPhone). Are you using an android device to run the nRF Connect app?

Yes, in deed.

I was wondering if you had any ideas as to why the problem exists on all but the nrfConnect on android? I have tried many other libraries, but particle’s is the only one with encryption built in. The nrfConnect app crashes for me on iPhone without any indication as to why

Unfortunately I’ve never really worked with BLE HID, so I cannot provide more guidance than this.
However, the fact that Windows detects driver issues when pairing to the device the same problem may cause the other apps to crash.

Since my wish for USB HID support on Gen3 also never materialized and now even BLE HID (as the closest substitute) doesn’t seem to work properly either it might be worth asking @Colleen if there may be any will on Particle’s side to jump in :wink:
(I hope at least Photon2 will support USB HID again).


my slightly altered test code
/*
 * Project MFISwitch
 * Description:
 * Author:
 * Date:
 */

SYSTEM_THREAD(ENABLED);
//SYSTEM_MODE(MANUAL);

SerialLogHandler logHandler(LOG_LEVEL_TRACE);

BleUuid genericAccessService(BLE_SIG_UUID_GENERIC_ACCESS_SVC);
BleUuid humanInterfaceDeviceService(BLE_SIG_UUID_HUMAN_INTERFACE_DEVICE_SVC);
BleUuid batteryLevelService(BLE_SIG_UUID_BATTERY_SVC);
BleUuid deviceInfoService(BLE_SIG_UUID_DEVICE_INFORMATION_SVC);

BleCharacteristic batteryLevelCharacteristic("bat", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A19), batteryLevelService);
BleCharacteristic pnpIDCharacteristic("pnp", BleCharacteristicProperty::READ, BleUuid(0x2A50), deviceInfoService);
BleCharacteristic hidInfoCharacteristic("hidInfo", BleCharacteristicProperty::READ, BleUuid(0x2A4A), humanInterfaceDeviceService);
BleCharacteristic protocolModeCharacteristic("protocol", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4E), humanInterfaceDeviceService);
BleCharacteristic reportMapCharacteristic("reportMap", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4B), humanInterfaceDeviceService);
BleCharacteristic reportCharacteristic("report", BleCharacteristicProperty::NOTIFY, BleUuid(0x2A4D), humanInterfaceDeviceService);

void configureBLE() {
  BLE.addCharacteristic(batteryLevelCharacteristic);
  BLE.addCharacteristic(pnpIDCharacteristic);
  BLE.addCharacteristic(hidInfoCharacteristic);
  BLE.addCharacteristic(protocolModeCharacteristic);
  BLE.addCharacteristic(reportMapCharacteristic);
  BLE.addCharacteristic(reportCharacteristic);

  BleAdvertisingData advertisementData;
  advertisementData.appendLocalName("Boron-4RP8TU"); //("MFI Switch");
  advertisementData.appendAppearance((ble_sig_appearance_t) BLE_APPEARANCE_HID_KEYBOARD);
  advertisementData.appendServiceUUID(humanInterfaceDeviceService);
  BLE.advertise(&advertisementData);
}

#define __ISR__(val) []()           \
{ static uint32_t ms = 0;           \
  if (millis() - ms < 125) return;  \
  ms = millis();                    \
  lastBtn = val;                    \
}

typedef void(*const isr)();
volatile int lastBtn    = 0;
const    isr fn[]       = { __ISR__(0x04), __ISR__(0x05), __ISR__(0x06), __ISR__(0x07) }; // key codes for a,b,c,d
const    int pins[]     = {         D0,         D1,         D2,         D3 };
const    int count      = sizeof(pins) / sizeof(pins[0]);

// setup() runs once, when the device is first turned on.
void setup() {
  const uint8_t pnpID[7] = { 0x02, 0x04, 0x6D, 0xC5, 0x2B, 0x00, 0x01 };
  pnpIDCharacteristic.setValue(pnpID, 7);

  const uint8_t hidInfo[4] = { 0x01, 0x11, 0x00, 0x21 };
  hidInfoCharacteristic.setValue(hidInfo, 4);

  const uint8_t protocolMode[1] = { 0x01 };
  protocolModeCharacteristic.setValue(protocolMode, 1);

  const uint8_t reportMap[] = {
        0x05, 0x01 , 	// Usage Page (Generic Desktop)
        0x09, 0x06 , 	// Usage (Keyboard)
        0xa1, 0x01 , 	// Collection (Application)
        0x05, 0x07 , 	// Usage Page (Keyboard)
        0x19, 0xe0 , 	// Usage Minimum (Keyboard LeftControl)
        0x29, 0xe7 , 	// Usage Maximum (Keyboard Right GUI)
        0x15, 0x00 , 	// Logical Minimum (0)
        0x25, 0x01 , 	// Logical Maximum (1)
        0x75, 0x01 , 	// Report Size (1)
        0x95, 0x08 , 	// Report Count (8)
        0x81, 0x02 , 	// Input (Data, Variable, Absolute) Modifier byte
        0x95, 0x01 , 	// Report Count (1)
        0x75, 0x08 , 	// Report Size (8)
        0x81, 0x01 , 	// Input (Constant) Reserved byte
        0x95, 0x06 , 	// Report Count (6)
        0x75, 0x08 , 	// Report Size (8)
        0x15, 0x00 , 	// Logical Minimum (0)
        0x25, 0x65 , 	// Logical Maximum (101)
        0x05, 0x07 , 	// Usage Page (Key Codes)
        0x05, 0x01 , 	// Usage Minimum (Reserved (no event indicated))
        0x05, 0x01 , 	// Usage Maximum (Keyboard Application)
        0x05, 0x01 , 	// Input (Data,Array) Key arrays (6 bytes)

        0xC0        // End Collection (Application)
    };
  reportMapCharacteristic.setValue(reportMap, sizeof(reportMap));

  BLE.on();
  BLE.onConnected([](const particle::BlePeerDevice&){ Log.info("Connecting"); } );
  configureBLE();

  for (static int p = 0; p < count; p++) {
    pinMode(pins[p], INPUT_PULLDOWN);
    attachInterrupt(pins[p], fn[p], RISING);
  }
}

void loop() {
  if (BLE.connected()) {
    static uint32_t msLastReport = 0;
    if (millis() - msLastReport > 1000) {
      msLastReport = millis();
      uint8_t report[] = {0x01, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00};      // report 'z' every second
      reportCharacteristic.setValue(report, sizeof(report));
    }
    
    if (lastBtn > 0) { 
      uint8_t report[] = {0x01, 0x00, lastBtn, 0x00, 0x00, 0x00, 0x00, 0x00};
      reportCharacteristic.setValue(report, sizeof(report));
    }
  }

  if (lastBtn > 0) { 
    Log.info("%d was pressed", lastBtn);
    lastBtn *= -1; // mark as handled 
  }
}