earlephilhower / arduino-pico

Raspberry Pi Pico Arduino core, for all RP2040 boards
GNU Lesser General Public License v2.1
1.88k stars 394 forks source link

Bluetooth LE Notifications not working #2231

Closed ullibak closed 3 weeks ago

ullibak commented 4 weeks ago

First of all, thanks to Earle for creating and maintaining this beautiful library!

I have a device that sends data through BLE notifications. I used the example BTStack -> LECentral and modified it for the service and characteristic UUIDs of my device. I was able to connect and read services and characteristics. I was also able to subscribe to notifications and the status value of the gattSubscribedCallback() function suggests that the notification was successfully registered. But the gattCharacteristicNotification() callback routine was never triggered when my device sent data.

I then compared to the BLE client and server temperature measurement example in the Pico SDK (\pico-examples\pico_w\bt\standalone\server.c and client.c) and found that maybe something is missing in the subscribeForNotifications() function in BTStackLib.cpp

After I modified this function, the notification callback routine was triggered and everything seemed to work as expected.

int BTstackManager::subscribeForNotifications(BLEDevice * device, BLECharacteristic * characteristic) {
    gattAction = gattActionSubscribe;
    // ADDED
    static gatt_client_notification_t notification_listener;
    gatt_client_listen_for_characteristic_value_updates(&notification_listener, gatt_client_callback,  device->getHandle(), (gatt_client_characteristic_t*) characteristic->getCharacteristic());
    // END ADDED
    return gatt_client_write_client_characteristic_configuration(gatt_client_callback, device->getHandle(), (gatt_client_characteristic_t*) characteristic->getCharacteristic(),
            GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION);
}

This is probably not the right way to fix this issue, but could maybe be a hint to a proper solution. The same solution should then also be applied to the subscribeForIndications() function and maybe also the corresponding unsubscribe functions.

Many thanks!

earlephilhower commented 4 weeks ago

Thanks for the debug and proposed solution! Why not throw together a PR with your suggestion, since you have a use case and came up with the fix? It may not be perfect, but if what is there now doesn't work (and the BTStack guys haven't kept up with their Arduino library so it's not going to be fixed upstream) we aren't likely to be worse off.

earlephilhower commented 4 weeks ago

Looking at the code, I think you're spot on with the fix, but the static local variable in a member function will get overwritten by every call to subscribeForXXX. Some sort of BLECharacteristic <-> gatt_client_notification_t map needs to be tracked, or malloc'd on every subscription to keep it from trouncing priors...

ullibak commented 3 weeks ago

In the meantime, I made some modifications that might better fit to the structure of BTstacklib.

I reverted the subscribeForNotifications function to its original version and added an additional function called listenForCharacteristicValueUpdates.

To do this, I had to add

int BLEDevice::listenForCharacteristicValueUpdates(BLECharacteristic * characteristic){
    return BTstack.listenForCharacteristicValueUpdates(this, characteristic);

and

int BTstackManager::listenForCharacteristicValueUpdates(BLEDevice * device, BLECharacteristic * characteristic) {
    gatt_client_listen_for_characteristic_value_updates(&notification_listener, gatt_client_callback,  device->getHandle(), 
            (gatt_client_characteristic_t*) characteristic->getCharacteristic());
    return 1;

to BTstacklib.cpp as well as the corresponding function definitions to BTstacklib.h

To circumvent the problem with the local variable, I added the definition

static gatt_client_notification_t notification_listener;

at the beginning of BTstacklib.cpp

In the ino-file (derived from the LECentral example), the new function is called before the original subscription function:

device->listenForCharacteristicValueUpdates(&characteristics[charMeasurement].characteristic);
device->subscribeForNotifications(&characteristics[charMeasurement].characteristic);

It works, but I have to state that I am by no means a proficient programmer nor am I an expert on BLE. This code was derived from a simple "copycat"-approach. Particularly, I do not understand the bookkeeping with the variable "notification_listener". In the example of the pico SDK, a function named

gatt_client_stop_listening_for_characteristic_value_updates(&notification_listener);

is called at disconnect. My code works without this, but I suppose that there is a reason for calling this function. My programming knowledge does not allow me to implement this function in a senseful way, so help would be appreciated...

ullibak commented 3 weeks ago

Found that the wrapper has been derived from the Arduino port that BlueKitchen published (a long time ago...).

Therefore, posted the issue there (https://github.com/bluekitchen/btstack/issues/615) hoping that they know exactliy how to implement the subscription for notifications correctly.

gonzob commented 3 weeks ago

Thanks also to Earle. I've been tearing my hair out over this exact problem for the last few days. I'm trying to build a rpi pico central for MIDI messages from an instrument. With the LECentral code it would connect, then nothing... I confirm that Ullibak's original post works for me, and I await any developments. Thanks. Gonzo

earlephilhower commented 3 weeks ago

Can either of you post a simple example that I can try locally without and with the fixes? I'm fine doing the PR here, since the Bluestack Arduino library is basically abandoned (but mostly working!).

gonzob commented 3 weeks ago

As i mentioned above my application is BLE MIDI. Here's my modified LECentral code:

#include <BTstackLib.h>
#include <SPI.h>

/*
   EXAMPLE_START(LECentral): LE Central

   @text Compared with the other examples, the LE Central is
   a bit more complex. This is because it performs multiple
   steps in sequence as it is common with GATT Client APIs.

   It shows how to first scan for other
   devices and then connect to one. When connected, a series of
   GATT Client operations are performed: first the list of
   GATT Services is queried. If a particular service is found,
   the list of its GATT Characteristics is retrieved and a set
   of known Characteristics are cached for later access.
*/

/*
   @section Characteristic Summary
   @text As multiple Characteristics need to be found, a custom
   struct is used to collect all information about it. This allows
   to define the list of necessary characteristics in the
   characteristics[] array
*/
/* LISTING_START(LECentralSummary): Characteristic Summary */

// BLE Shield Service V2 incl. used Characteristics
//UUID bleShieldServiceV2UUID("B8E06067-62AD-41BA-9231-206AE80AB550");
UUID bleShieldServiceV2UUID("03b80e5a-ede8-4b33-a751-6ce34ec4c700");

typedef struct characteristic_summary {
  UUID         uuid;
  const char * name;
  bool         found;
  BLECharacteristic characteristic;
} characteristic_summary_t;

typedef enum characteristicIDs {
  charRX = 0,
  charTX,
  charBaud,
  charBdAddr,
  numCharacteristics  /* last one */
} characteristicIDs_t;

//characteristic_summary characteristics[] = {
//  { UUID("f897177b-aee8-4767-8ecc-cc694fd5fcee"), "RX", false, BLECharacteristic() },
//  { UUID("bf45e40a-de2a-4bc8-bba0-e5d6065f1b4b"), "TX", false, BLECharacteristic() },
//  { UUID("2fbc0f31-726a-4014-b9fe-c8be0652e982"), "Baudrate", false, BLECharacteristic() },
//  { UUID("65c228da-bad1-4f41-b55f-3d177f4e2196"), "BD ADDR", false, BLECharacteristic() }
//};

characteristic_summary characteristics[] = {
  { UUID("7772e5db-3868-4112-a1a9-f2669d106bf3"), "MIDI", false, BLECharacteristic() }
};

/* LISTING_END(LECentralSummary): Characteristic Summary */

// Application state
BLEDevice  myBLEDevice;
BLEService myBLEService;
bool serviceFound;
bool sendCounter = false;

int counter = 0;
char counterString[20];

// static btstack_timer_source_t heartbeat;

/*
   @section Setup
   @text In the setup, various callbacks are registered. After that
   we start scanning for other devices
*/
/* LISTING_START(LECentralSetup): LE Central Setup */
void setup(void) {
  Serial.begin(9600);
  BTstack.setBLEAdvertisementCallback(advertisementCallback);
  BTstack.setBLEDeviceConnectedCallback(deviceConnectedCallback);
  BTstack.setBLEDeviceDisconnectedCallback(deviceDisconnectedCallback);
  BTstack.setGATTServiceDiscoveredCallback(gattServiceDiscovered);
  BTstack.setGATTCharacteristicDiscoveredCallback(gattCharacteristicDiscovered);
  BTstack.setGATTCharacteristicNotificationCallback(gattCharacteristicNotification);
  BTstack.setGATTCharacteristicReadCallback(gattReadCallback);
  BTstack.setGATTCharacteristicWrittenCallback(gattWrittenCallback);
  BTstack.setGATTCharacteristicSubscribedCallback(gattSubscribedCallback);
  BTstack.setup();
  BTstack.bleStartScanning();
}
/* LISTING_END(LECentralSetup): LE Central Setup */

/*
   @section Loop

   @text In the standard Arduino loop() function, BTstack's loop() is called first
   If we're connected, we send the string "BTstack" plus a counter as fast as possible.
   As the Bluetooth module might be busy, it's important to check the result of the
   writeCharacteristicWithoutResponse() call. If it's not ok, we just try again in the
   next loop iteration.
*/
/* LISTING_START(LECentralLoop): Loop */
void loop(void) {
  BTstack.loop();

  // send counter as fast as possible
  if (sendCounter) {
    sprintf(counterString, "BTstack %u\n", counter);
    int result = myBLEDevice.writeCharacteristicWithoutResponse(&characteristics[charTX].characteristic, (uint8_t*) counterString, strlen(counterString));
    if (result == 0) {
      Serial.print("Wrote without response: ");
      Serial.println(counterString);
      counter++;
    }
  }
}
/* LISTING_END(LECentralLoop): Loop */

/*
   @section Advertisement Callback

   @text When an Advertisement is received, we check if it contains
   the UUID of the service we're interested in. Only a single service
   with a 128-bit UUID can be contained in and Advertisement and not
   all BLE devices provides this. Other options are to match on the
   reported device name or the BD ADDR prefix.

   If we found an interesting device, we try to connect to it.
*/
/* LISTING_START(LECentralAdvertisementCallback): Advertisement Callback */
void advertisementCallback(BLEAdvertisement *bleAdvertisement) {
  Serial.print("Device discovered: ");
  Serial.print(bleAdvertisement->getBdAddr()->getAddressString());
  Serial.print(", RSSI: ");
  Serial.println(bleAdvertisement->getRssi());
  if (bleAdvertisement->containsService(&bleShieldServiceV2UUID)) {
    Serial.println("\nBLE ShieldService V2 found!\n");
    BTstack.bleStopScanning();
    BTstack.bleConnect(bleAdvertisement, 10000);  // 10 s
  }
}
/* LISTING_END(LECentralAdvertisementCallback): Advertisement Callback */

/*
   @section Device Connected Callback

   @text At the end of bleConnect(), the device connected callback is callec.
   The status argument tells if the connection timed out, or if the connection
   was established successfully.

   On a successful connection, a GATT Service Discovery is started.
*/
/* LISTING_START(LECentralDeviceConnectedCallback): Device Connected Callback */
void deviceConnectedCallback(BLEStatus status, BLEDevice *device) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.println("Device connected!");
      myBLEDevice = *device;
      counter = 0;
      myBLEDevice.discoverGATTServices();
      break;
    case BLE_STATUS_CONNECTION_TIMEOUT:
      Serial.println("Error while Connecting the Peripheral");
      BTstack.bleStartScanning();
      break;
    default:
      break;
  }
}
/* LISTING_END(LECentralDeviceConnectedCallback): Device Connected Callback */

/*
   @section Device Disconnected Callback

   @text If the connection to a device breaks, the device disconnected callback
   is called. Here, we start scanning for new devices again.
*/
/* LISTING_START(LECentralDeviceDisconnectedCallback): Device Disconnected Callback */
void deviceDisconnectedCallback(BLEDevice * device) {
  (void) device;
  Serial.println("Disconnected, starting over..");
  sendCounter = false;
  BTstack.bleStartScanning();
}
/* LISTING_END(LECentralDeviceDisconnectedCallback): Device Disconnected Callback */

/*
   @section Service Discovered Callback

   @text The service discovered callback is called for each service and after the
   service discovery is complete. The status argument is provided for this.

   The main information about a discovered Service is its UUID.
   If we find our service, we store the reference to this service.
   This allows to discover the Characteristics for our service after
   the service discovery is complete.
*/
/* LISTING_START(LECentralServiceDiscoveredCallback): Service Discovered Callback */
void gattServiceDiscovered(BLEStatus status, BLEDevice *device, BLEService *bleService) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.print("Service Discovered: :");
      Serial.println(bleService->getUUID()->getUuidString());
      if (bleService->matches(&bleShieldServiceV2UUID)) {
        serviceFound = true;
        Serial.println("Our service located!");
        myBLEService = *bleService;
      }
      break;
    case BLE_STATUS_DONE:
      Serial.println("Service discovery finished");
      if (serviceFound) {
        device->discoverCharacteristicsForService(&myBLEService);
      }
      break;
    default:
      Serial.println("Service discovery error");
      break;
  }
}
/* LISTING_END(LECentralServiceDiscoveredCallback): Service Discovered Callback */

/*
   @section Characteristic Discovered Callback

   @text Similar to the Service Discovered callback, the Characteristic Discovered
   callback is called for each Characteristic found and after the discovery is complete.

   The main information is again its UUID. If we find a Characteristic that we're
   interested in, it's name is printed and a reference stored for later.

   On discovery complete, we subscribe to a particular Characteristic to receive
   Characteristic Value updates in the Notificaation Callback.
*/
/* LISTING_START(LECentralCharacteristicDiscoveredCallback): Characteristic Discovered Callback */
void gattCharacteristicDiscovered(BLEStatus status, BLEDevice *device, BLECharacteristic *characteristic) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.print("Characteristic Discovered: ");
      Serial.print(characteristic->getUUID()->getUuidString());
      Serial.print(", handle 0x");
      Serial.println(characteristic->getCharacteristic()->value_handle, HEX);
      int i;
      for (i = 0; i < numCharacteristics; i++) {
        if (characteristic->matches(&characteristics[i].uuid)) {
          Serial.print("Characteristic found: ");
          Serial.println(characteristics[i].name);
          characteristics[i].found = 1;
          characteristics[i].characteristic = *characteristic;
          break;
        }
      }
      break;
    case BLE_STATUS_DONE:
      Serial.print("Characteristic discovery finished, status ");
      Serial.println(status, HEX);
      if (characteristics[charRX].found) {
        device->subscribeForNotifications(&characteristics[charRX].characteristic);
        Serial.println(characteristics[charRX].name);
      }
      break;
    default:
      Serial.println("Characteristics discovery error");
      break;
  }
}
/* LISTING_END(LECentralCharacteristicDiscoveredCallback): Characteristic Discovered Callback */

/*
   @section Subscribed Callback

   @text After the subscribe operation is complete, we get notified if it was
   successful. In this example, we read the Characteristic that contains the
   BD ADDR of the other device. This isn't strictly necessary as we already
   know the device address from the Advertisement, but it's a common pattern
   with iOS as the device address is hidden from applications.
*/
/* LISTING_START(LECentralSubscribedCallback): Subscribed Callback */
void gattSubscribedCallback(BLEStatus status, BLEDevice * device) {
  (void) status;
  device->readCharacteristic(&characteristics[charBdAddr].characteristic);
}
/* LISTING_END(LECentralSubscribedCallback): Subscribed Callback */

/*
   @section Read Callback

   @text The Read callback is called with the result from a read operation.
   Here, we write to the TX Characteristic next.
*/
/* LISTING_START(LECentralReadCallback): Read Callback */
void gattReadCallback(BLEStatus status, BLEDevice *device, uint8_t *value, uint16_t length) {
  (void) status;
  (void) length;
  Serial.print("Read callback: ");
  Serial.println((const char *)value);
  device->writeCharacteristic(&characteristics[charTX].characteristic, (uint8_t*) "Hello!", 6);
}
/* LISTING_END(LECentralReadCallback): Read Callback */

/*
   @section Written Callback

   @text After the write operation is complete, the Written Callback is callbed with
   the result in the status argument. As we're done with the initial setup of the remote
   device, we set the flag to write the test string as fast as possible.
*/
/* LISTING_START(LECentralWrittenCallback): Written Callback */
void gattWrittenCallback(BLEStatus status, BLEDevice *device) {
  (void) status;
  (void) device;
  sendCounter = true;
}
/* LISTING_END(LECentralWrittenCallback): Written Callback */

/*
   @section Notification Callback

   @text Notifications for Characteristic Value Updates are delivered via the
   Notification Callback. When more than one Characteristic is subscribed,
   the value handle can be used to distinguish between them. The
   BLECharacteristic.isValueHandle(int handle) allows to test if a value handle
   belongs to a particular Characteristic.
*/
/* LISTING_START(LECentralNotificationCallback): Notification Callback */
void gattCharacteristicNotification(BLEDevice *device, uint16_t value_handle, uint8_t *value, uint16_t length) {
  (void) device;
  (void) value_handle;
  (void) length;
  //Serial.print("Notification: ");
  //Serial.println((const char *)value);
  // parse the BLE message into normal MIDI
  // ignore first two bytes of the message,
  // then save the bytes, ignoring timing bytes
  int j = 0; // count the number of bytes
  for (int i = 2; i < length;){   
      Serial.print((byte)value[i]); // Status byte
      i += 1;
      j += 1;
      // detect the timing byte
      while ((byte)value[i] < 128 and i < length and j < 15){ 
        Serial.print((byte)value[i]);
        i += 1;
        j += 1;
      }
      i +=1; //skip the timing byte
    }
    Serial.println("");

}
/* LISTING_END(LECentralNotificationCallback): Notification Callback */`

To test it you'll need a MIDI perpheral such as a keyboard, or wind controller. The notification callback is a work in progress so it's not very flash...

What I'm intending to do wit this project is to receive BLE MIDI from an instrument (wind controller) and pump it into a synth which has a USB port. This means that the microcontroller needs to be able to present a USB-MIDI port to the synth. I originally tried to do this in MicroPython (because I know it can do the USB-MIDI) but I couldn't get any notifications from the instrument to appear. In frustration I swapped to the Arduino IDE, but had the same problem. After I got it working last night (Thankyou!), I then switched to getting the USB-MIDI port working, and it appears that there's no code for that.....

Gonzo

ullibak commented 3 weeks ago

Hi I can post my example that I used for debugging. I started with the example of a temperature sensor found in the example directory of the pico SDK: C:\pico\pico-examples\pico_w\bt\standalone and compiled it using VSCode.

The example code "server.c" sends a temperature measurement derived from an internal diode every couple of seconds through BLE notification. I modified this code (and the makefile) to send a temperature reading every 5 sec. and also output the temperature measurement and the device's address via USB. To get the device running, simply copy the UF2 file in this zip archive to the pico and hook it up to some terminal at 115200 bd, e.g. the Serial Monitor of the Arduino IDE: picow_ble_temp_sensor.zip

The output should be something like

Write temp 19.55 degc
sent from address D8:3A:DD:20:70:07

repeated every 5 seconds.

Then, take a second Pico device and copy the code below (derived from the LECentral example) to the arduino IDE. Change the address in the variable addrString to the address that the first Pico showed. Compile, upload and look at the output of the Serial Monitor. The program looks at all the advertisments of BLE devices and if the address of the Pico temperature sensor is found, it connects and subsctribes to notifications.

#include <BTstackLib.h>
#include <SPI.h>
#include <time.h>

typedef struct characteristic_summary {
  UUID         uuid;
  const char * name;
  bool         found;
  BLECharacteristic characteristic;
} characteristic_summary_t;

typedef enum characteristicIDs {
  charMeasurement = 0,
  numCharacteristics  
} characteristicIDs_t;

// Enter address of Pico temperature sensor here:
char* addrString = "D8:3A:DD:20:70:07"; 

UUID myServiceUuid("0000181A-0000-1000-8000-00805F9B34FB"); // Micropython temperature example from Pico SDK

characteristic_summary characteristics[] = {
  { UUID("00002A6E-0000-1000-8000-00805F9B34FB"), "Measurement", false, BLECharacteristic()} // Micropython temperature example from Pico SDK
};        

time_t now, old_time;

BLEDevice  myBLEDevice;
BLEService myBLEService;
const int maxServices = 50; //max. 50 services should be enough
int numServicesFound = 0; //cout number of services found
BLEService foundServices[maxServices]; //max. 50 services should be enough

bool serviceFound;
bool characteristicFound = false;
bool sendCounter = false;
bool LED_status = false;

static gatt_client_notification_t notification_listener;

void setup(void) {
  Serial.begin(115200);
  while(!Serial){}; // wait for serial connection. Remove for real application...
  pinMode(LED_BUILTIN, OUTPUT);
  BTstack.setBLEAdvertisementCallback(advertisementCallback);
  BTstack.setBLEDeviceConnectedCallback(deviceConnectedCallback);
  BTstack.setBLEDeviceDisconnectedCallback(deviceDisconnectedCallback);
  BTstack.setGATTServiceDiscoveredCallback(gattServiceDiscovered);
  BTstack.setGATTCharacteristicDiscoveredCallback(gattCharacteristicDiscovered);
  BTstack.setGATTCharacteristicNotificationCallback(gattCharacteristicNotification);
  BTstack.setGATTCharacteristicIndicationCallback(gattCharacteristicIndication);
  BTstack.setGATTCharacteristicReadCallback(gattReadCallback);
  BTstack.setGATTCharacteristicWrittenCallback(gattWrittenCallback);
  BTstack.setGATTCharacteristicSubscribedCallback(gattSubscribedCallback);
  BTstack.setup();
  BTstack.bleStartScanning();
}

/*
   @section Loop

   @text In the standard Arduino loop() function, BTstack's loop() is called first
   If we're connected, we send the string "BTstack" plus a counter as fast as possible.
   As the Bluetooth module might be busy, it's important to check the result of the
   writeCharacteristicWithoutResponse() call. If it's not ok, we just try again in the
   next loop iteration.
*/
/* LISTING_START(LECentralLoop): Loop */
void loop(void) {
  BTstack.loop();

  time(&now);
  if(now >= old_time + 1){
    old_time = now;
    if (LED_status == false){
      digitalWrite(LED_BUILTIN, HIGH);
      LED_status = true;
    } else {
      digitalWrite(LED_BUILTIN, LOW);
      LED_status = false;
    }
  }

}

/*
   @section Advertisement Callback

   @text When an Advertisement is received, we check if it contains
   the UUID of the service we're interested in. Only a single service
   with a 128-bit UUID can be contained in and Advertisement and not
   all BLE devices provides this. Other options are to match on the
   reported device name or the BD ADDR prefix.

   If we found an interesting device, we try to connect to it.
*/
/* LISTING_START(LECentralAdvertisementCallback): Advertisement Callback */
void advertisementCallback(BLEAdvertisement *bleAdvertisement) {
  Serial.print("Device discovered. Addr: ");
  Serial.print(bleAdvertisement->getBdAddr()->getAddressString());
  Serial.print(", UUID: ");
  Serial.print(bleAdvertisement->getIBeaconUUID()->getUuid128String());
  Serial.print(", RSSI: ");
  Serial.println(bleAdvertisement->getRssi());

  if (strcmp(bleAdvertisement->getBdAddr()->getAddressString(), addrString) == 0){
    Serial.print("Address ");
    Serial.print(addrString);
    Serial.println(" found!");

    BTstack.bleStopScanning();
    BTstack.bleConnect(bleAdvertisement, 10000);  // 10 s
  }
}

/*
   @section Device Connected Callback

   @text At the end of bleConnect(), the device connected callback is callec.
   The status argument tells if the connection timed out, or if the connection
   was established successfully.

   On a successful connection, a GATT Service Discovery is started.
*/
/* LISTING_START(LECentralDeviceConnectedCallback): Device Connected Callback */
void deviceConnectedCallback(BLEStatus status, BLEDevice *device) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.println("Device connected!");
      myBLEDevice = *device;
      myBLEDevice.discoverGATTServices();
      break;
    case BLE_STATUS_CONNECTION_TIMEOUT:
      Serial.println("Error while Connecting the Peripheral");
      BTstack.bleStartScanning();
      break;
    default:
      break;
  }
}

/*
   @section Device Disconnected Callback

   @text If the connection to a device breaks, the device disconnected callback
   is called. Here, we start scanning for new devices again.
*/
/* LISTING_START(LECentralDeviceDisconnectedCallback): Device Disconnected Callback */
void deviceDisconnectedCallback(BLEDevice * device) {
  (void) device;
  Serial.println("Disconnected, starting over..");
  sendCounter = false;
  characteristicFound = false;
  BTstack.bleStartScanning();
}

/*
   @section Service Discovered Callback

   @text The service discovered callback is called for each service and after the
   service discovery is complete. The status argument is provided for this.

   The main information about a discovered Service is its UUID.
   If we find our service, we store the reference to this service.
   This allows to discover the Characteristics for our service after
   the service discovery is complete.
*/
/* LISTING_START(LECentralServiceDiscoveredCallback): Service Discovered Callback */
void gattServiceDiscovered(BLEStatus status, BLEDevice *device, BLEService *bleService) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.print("Service Discovered. UUID String:");
      Serial.print(bleService->getUUID()->getUuidString());
      Serial.print(", UUID128 String:");
      Serial.println(bleService->getUUID()->getUuid128String());
      if (bleService->matches(&myServiceUuid)) {
        serviceFound = true;
        Serial.println("Service located");
        myBLEService = *bleService;
      }
      break;
    case BLE_STATUS_DONE:
      Serial.println("Service discovery finished");
      if (serviceFound) {
        Serial.println("Requested Service found. Search for Characteristics...");
        device->discoverCharacteristicsForService(&myBLEService);
      } 
      break;
    default:
      Serial.println("Service discovery error");
      break;
  }
}

/*
   @section Characteristic Discovered Callback

   @text Similar to the Service Discovered callback, the Characteristic Discovered
   callback is called for each Characteristic found and after the discovery is complete.

   The main information is again its UUID. If we find a Characteristic that we're
   interested in, it's name is printed and a reference stored for later.

   On discovery complete, we subscribe to a particular Characteristic to receive
   Characteristic Value updates in the Notificaation Callback.
*/
void gattCharacteristicDiscovered(BLEStatus status, BLEDevice *device, BLECharacteristic *characteristic) {
  switch (status) {
    case BLE_STATUS_OK:
      Serial.print("Characteristic Discovered: UUID String:");
      Serial.print(characteristic->getUUID()->getUuidString());
      Serial.print(", UUID128 String: ");
      Serial.print(characteristic->getUUID()->getUuid128String());
      Serial.print(", handle: 0x");
      Serial.print(characteristic->getCharacteristic()->value_handle, HEX);
      Serial.print(", properties: ");
      Serial.println(characteristic->getCharacteristic()->properties);

      int i;
      for (i = 0; i < numCharacteristics; i++) {
        if (characteristic->matches(&characteristics[i].uuid)) {
          Serial.print("Characteristic found: ");
          Serial.println(characteristics[i].name);
          characteristics[i].found = 1;
          characteristics[i].characteristic = *characteristic;
          break;
        }
      }

      break;
    case BLE_STATUS_DONE:
      Serial.print("Characteristic discovery finished, status ");
      Serial.println(status, HEX);

      if (characteristics[charMeasurement].found) {       
        Serial.println("Subscribe to Notifications");

        device->subscribeForNotifications(&characteristics[charMeasurement].characteristic);

        characteristicFound = true;
      } 
      break;
    default:
      Serial.println("Characteristics discovery error");
      break;
  }
}

/*
   @section Subscribed Callback

   @text After the subscribe operation is complete, we get notified if it was
   successful. In this example, we read the Characteristic that contains the
   BD ADDR of the other device. This isn't strictly necessary as we already
   know the device address from the Advertisement, but it's a common pattern
   with iOS as the device address is hidden from applications.
*/
/* LISTING_START(LECentralSubscribedCallback): Subscribed Callback */
void gattSubscribedCallback(BLEStatus status, BLEDevice * device) {
  (void) status;
  Serial.print("In gattSubscribedCallback(). Status: ");
  Serial.print(status);
  Serial.print(", Handle: 0x");
  Serial.println(device->getHandle());
}

/*
   @section Read Callback

   @text The Read callback is called with the result from a read operation.
   Here, we write to the TX Characteristic next.
*/
void gattReadCallback(BLEStatus status, BLEDevice *device, uint8_t *value, uint16_t length) {
  (void) status;
  //(void) length;
  Serial.print("Read callback. Data Length: ");
  Serial.print(length);
  Serial.print(",  Value: ");

  for(int n = 0; n < length; n++){
  Serial.print(*(value + n), HEX);
  Serial.print(" ");
}
Serial.println();
}

/*
   @section Written Callback

   @text After the write operation is complete, the Written Callback is called with
   the result in the status argument. As we're done with the initial setup of the remote
   device, we set the flag to write the test string as fast as possible.
*/

void gattWrittenCallback(BLEStatus status, BLEDevice *device) {
  //(void) status;
  (void) device;
  sendCounter = true;
  Serial.print("In gattWrittenCallback, status: ");
  Serial.print(status);
  Serial.print(", handle: 0x");
  Serial.println(device->getHandle(), HEX);
}

/*
   @section Notification Callback

   @text Notifications for Characteristic Value Updates are delivered via the
   Notification Callback. When more than one Characteristic is subscribed,
   the value handle can be used to distinguish between them. The
   BLECharacteristic.isValueHandle(int handle) allows to test if a value handle
   belongs to a particular Characteristic.
*/

void gattCharacteristicNotification(BLEDevice *device, uint16_t value_handle, uint8_t *value, uint16_t length) {
  (void) device;
  (void) value_handle;
  //(void) length;
  Serial.print("Notification received. Length: ");
  Serial.print(length);
  Serial.print(", handle: 0x");
  Serial.print(value_handle, HEX);
  Serial.print(" ");

  if (length == 2) { // Temperature for Pico temperature server example
    float temp = little_endian_read_16(value, 0);
    Serial.printf("Temperature %.2f degc\n", temp / 100);
  } else {
    printf("Unexpected length %d\n", length);
    for(int n = 0; n < length; n++){
    printf("%X ", *(value + n));
    }
  }
}

void gattCharacteristicIndication(BLEDevice *device, uint16_t value_handle, uint8_t *value, uint16_t length) {
  (void) device;
  //(void) value_handle;
  //(void) length;
  Serial.print("Indication received. Length: ");
  Serial.print(length);
  Serial.print(", handle:");
  Serial.print(value_handle);
  Serial.print(", value:");
  Serial.println((const char *)value);
}

The output of the program is something like:

Device discovered. Addr: FD:73:FB:37:FF:A7, UUID: 606BD402-0002-0104-0000-00A7FE011004, RSSI: -94
Device discovered. Addr: EE:BB:F0:A5:E8:1C, UUID: 0000001D-1109-5334-524B-41323334394B, RSSI: -96
Device discovered. Addr: EE:BB:F0:A5:E8:1C, UUID: 11079ECA-DC24-0EE5-A9E0-93F3A3B50100, RSSI: -97
Device discovered. Addr: EE:BB:F0:A5:E8:1C, UUID: 0000001D-1109-5334-524B-41323334394B, RSSI: -100
Device discovered. Addr: 40:C9:C3:5E:51:45, UUID: 0010077E-1F9B-05FD-C9A8-00A7FE011004, RSSI: -96
Device discovered. Addr: 40:C9:C3:5E:51:45, UUID: 606BD402-0002-0104-0000-00A7FE011004, RSSI: -97
Device discovered. Addr: D8:3A:DD:20:70:07, UUID: 2044383A-3341-3A44-443A-32303A37303A, RSSI: -43
Address D8:3A:DD:20:70:07 found!
Device connected!
Service Discovered. UUID String:1800, UUID128 String:00001800-0000-1000-8000-00805F9B34FB
Service Discovered. UUID String:1801, UUID128 String:00001801-0000-1000-8000-00805F9B34FB
Service Discovered. UUID String:181a, UUID128 String:0000181A-0000-1000-8000-00805F9B34FB
Service located
Service discovery finished
Requested Service found. Search for Characteristics...
Characteristic Discovered: UUID String:2a6e, UUID128 String: 00002A6E-0000-1000-8000-00805F9B34FB, handle: 0x9, properties: 50
Characteristic found: Measurement
Characteristic discovery finished, status 1
Subscribe to Notifications
In gattSubscribedCallback(). Status: 0, Handle: 0x64

This means that the device with the given address has been found, connection has been established and the subscription to notifications was successful (status = 0).

Without any modifications to BTstack.cpp, the output stops there and the notification callback routine, that should output the temperature value, is never called.

With the modifications described above (i.e. adding the _gatt_client_listen_for_characteristic_valueupdates() function in some or the other way, the notification works as expected and the temperature is printed out every 5 seconds:

Notification received. Length: 2, handle: 0x9 Temperature 17.68 degc
Notification received. Length: 2, handle: 0x9 Temperature 17.21 degc
Notification received. Length: 2, handle: 0x9 Temperature 17.21 degc
Notification received. Length: 2, handle: 0x9 Temperature 18.61 degc
earlephilhower commented 3 weeks ago

Awesome, thanks for both the samples. I think with one characteristic, like you're running @ullibak , the static is fine, but if you added a 2nd one the BLE infrastructure will get confused. Let me poke around and see the best place to stuff that extra housekeeping needed.

Also, FWIW, the BTStack guys seem to say that the library code as-is is correct, w/o the add'l gatt_client_listen_for_characteristic_value_updates call. See their posting here. Not sure of what to make of it, but maybe things in the BTStack core have changed since their original posting. And if it works with the add'l call, I sure don't mind adding it. :)

ullibak commented 3 weeks ago

Thank you Earle!

In the link that you posted, Matthias writes (quote):

"Anyway, it's enough to call gatt_client_write_client_characteristic_configuration to enable notifications, no need to gatt_client_write_characteristic_descriptor manually.

To get notifications, you also have to register a listener for that characteristic with gatt_client_listen_for_characteristic_value_updates."

As I understand this, the gatt_client_listen_for_characteristic_value_updates() function is mandatory to get notifications.

Bluekitchen also uses this function e.g. in the example https://github.com/bluekitchen/btstack/blob/2b49e57bd1fae85ac32ac1f41cdb7c794de335f6/example/gatt_heart_rate_client.c#L178

just before subscribing to notifications. I suppose that it simply was forgotten when BlueKitchen created their Arduino port.

earlephilhower commented 3 weeks ago

2241 adds that new structure to every characteristic and uses it in the listen/unlisten methods. I could not find any calls that look similar for BLE indicators, and there are no instances of their use in the BTStack examples, so I have not added anything to them. Tested w/the BLE temp sensor MCVE posted here.

ullibak commented 3 weeks ago

@Earle: I see that you are already working on the solution. In the meantime, Matthias gave an input on how to implement a "general" listener: https://github.com/bluekitchen/btstack/issues/615