Examples

In this section, we will describe a number of examples that are available from inside the Arduino IDE via File->Examples->BTstack.

iBeacon: iBeacon Simulator

Setup

After BTstack.setup(), iBeaconConfigure() configures BTstack to send out iBeacons Advertisements with the provided Major ID, Minor ID and UUID.

UUID uuid("E2C56DB5-DFFB-48D2-B060-D0F5A71096E0");
void setup(void){
  Serial.begin(9600);
  BTstack.setup();
  BTstack.iBeaconConfigure(&uuid, 4711, 2);
  BTstack.startAdvertising();
}

iBeaconScanner: iBeacon Scanner

Setup

After BTstack.setup(), BTstack is configured to call advertisementCallback whenever an Advertisement was received. Then, a device discovery is started

void setup(void){
  Serial.begin(9600);
  BTstack.setup();
  BTstack.setBLEAdvertisementCallback(advertisementCallback);
  BTstack.bleStartScanning();
}

Advertisment Callback

Whenever an Advertisement is received, isIBeacon() checks if it contains an iBeacon. If yes, the Major ID, Minor ID, and UUID is printed. If it's not an iBeacon, only the BD_ADDR and the received signal strength (RSSI) is shown.

void advertisementCallback(BLEAdvertisement *adv) {
  if (adv->isIBeacon()) {
    Serial.print("iBeacon found ");
    Serial.print(adv->getBdAddr()->getAddressString());
    Serial.print(", RSSI ");
    Serial.print(adv->getRssi());
    Serial.print(", UUID ");
    Serial.print(adv->getIBeaconUUID()->getUuidString());
    Serial.print(", MajorID ");
    Serial.print(adv->getIBeaconMajorID());
    Serial.print(", MinorID ");
    Serial.print(adv->getIBecaonMinorID());
    Serial.print(", Measured Power ");
    Serial.println(adv->getiBeaconMeasuredPower());
  } else {
    Serial.print("Device discovered: ");
    Serial.print(adv->getBdAddr()->getAddressString());
    Serial.print(", RSSI ");
    Serial.println(adv->getRssi());
  }
}

ANCS: ANCS Client

An ANCS Client needs to include the ANCS UUID in its advertisement to get recognized by iOS

const uint8_t adv_data[] = {
  // Flags general discoverable
  0x02, 0x01, 0x02, 
  // Name
  0x05, 0x09, 'A', 'N', 'C', 'S', 
  // Service Solicitation, 128-bit UUIDs - ANCS (little endian)
  0x11,0x15,0xD0,0x00,0x2D,0x12,0x1E,0x4B,0x0F,0xA4,0x99,0x4E,0xCE,0xB5,0x31,0xF4,0x05,0x79
};

Setup

In the setup, the LE Security Manager is configured to accept pairing requests. Then, the ANCS Client library is initialized and and ancs_callback registered. Finally, the Advertisement data is set and Advertisements are started.

void setup(void){

  Serial.begin(9600);
  Serial.println("BTstack ANCS Client starting up...");

  // startup BTstack and configure log_info/log_error
  BTstack.setup();

  sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  sm_set_authentication_requirements( SM_AUTHREQ_BONDING );

  // setup ANCS Client
  ancs_client_init();
  ancs_client_register_callback(&ancs_callback);

  // enable advertisements
  BTstack.setAdvData(sizeof(adv_data), adv_data);
  BTstack.startAdvertising();
}

ANCS Callback

In the ANCS Callback, connect and disconnect events are received. For actual notifications, ancs_client_attribute_name_for_id allows to look up the name. To get the notification body, e.g., the actual message, the GATT Client needs to be used direclty.

void ancs_callback(ancs_event_t * event){
  const char * attribute_name;
  switch (event->type){
    case ANCS_CLIENT_CONNECTED:
      Serial.println("ANCS Client: Connected");
      break;
    case ANCS_CLIENT_DISCONNECTED:
      Serial.println("ANCS Client: Disconnected");
      break;
    case ANCS_CLIENT_NOTIFICATION:
      attribute_name = ancs_client_attribute_name_for_id(event->attribute_id);
      if (!attribute_name) break;
      Serial.print("Notification: ");
      Serial.print(attribute_name);
      Serial.print(" - ");
      Serial.println(event->text);
      break;
    default:
      break;
  }
}

LECentral: LE Central

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.

Characteristic Summary

As multiple Characteristics need to be found, a custom struct is used to collect all information about it. This allows to defined the list of neccessary characteristics in the characteristics[] array

// BLE Shield Service V2 incl. used Characteristics
UUID bleShieldServiceV2UUID("B8E06067-62AD-41BA-9231-206AE80AB550");

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

typedef enum characteristicIDs {
  charRX = 0,
  charTX,
  charBaud,
  charBdAddr,
} characteristicIDs_t;

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

Setup

In the setup, various callbacks are registered. After that we start scanning for other devices

 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();
}

Loop

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.

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 == BLE_PERIPHERAL_OK){
      Serial.print("Wrote without response: ");
      Serial.println(counterString);
      counter++;
    }
  }
}

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.

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
  }
}

Device Connected Callback

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.

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;
  }
}

Device Disconnected Callback

If the connection to a device breaks, the device disconnected callback is called. Here, we start scanning for new devices again.

void deviceDisconnectedCallback(BLEDevice * device){
  Serial.println("Disconnected, starting over..");
  sendCounter = false;
  BTstack.bleStartScanning();
}

Service Discovered Callback

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.

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;
  }
}

Characteristic Discovered Callback

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: ");
      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);
      }
      break;
    default:
      Serial.println("Characteristics discovery error");
      break;
  }
}

Subscribed Callback

After the subcribe 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 neccessary 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.

void gattSubscribedCallback(BLEStatus status, BLEDevice * device){
  device->readCharacteristic(&characteristics[charBdAddr].characteristic);
}

Read Callback

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) {
  Serial.print("Read callback: ");
  Serial.println((const char *)value);
  device->writeCharacteristic(&characteristics[charTX].characteristic, (uint8_t*) "Hello!", 6);
}

Written Callback

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.

void gattWrittenCallback(BLEStatus status, BLEDevice *device){
  sendCounter = true;
}

Notification Callback

Notifictions 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) {
  Serial.print("Notification: ");
  Serial.println((const char *)value);
}

LEPeripheral: LE Peripheral

Setup

BTstack allows to setup a GATT Services and Characteristics directly from the setup function without using other tools outside of the Arduino IDE.

First, a number of callbacks are set. Then, a Service with a Read-only Characteristic and a dynamic Characteristic is added to the GATT database. In BTstack, a dynamic Characteristic is a Characteristic where reads and writes are forwarded to the Sketch. In this example, the dynamic Characteristic is provided by the single byte variable characteristic_data.

static char characteristic_data = 'H';

void setup(void){

  Serial.begin(9600);

  // set callbacks
  BTstack.setBLEDeviceConnectedCallback(deviceConnectedCallback);
  BTstack.setBLEDeviceDisconnectedCallback(deviceDisconnectedCallback);
  BTstack.setGATTCharacteristicRead(gattReadCallback);
  BTstack.setGATTCharacteristicWrite(gattWriteCallback);

  // setup GATT Database
  BTstack.addGATTService(new UUID("B8E06067-62AD-41BA-9231-206AE80AB551"));
  BTstack.addGATTCharacteristic(new UUID("f897177b-aee8-4767-8ecc-cc694fd5fcef"), ATT_PROPERTY_READ, "This is a String!");
  BTstack.addGATTCharacteristicDynamic(new UUID("f897177b-aee8-4767-8ecc-cc694fd5fce0"), ATT_PROPERTY_READ | ATT_PROPERTY_WRITE | ATT_PROPERTY_NOTIFY, 0);

  // startup Bluetooth and activate advertisements
  BTstack.setup();
  BTstack.startAdvertising();
}

Device Connected Callback

When a remove device connects, device connected callback is callec.

void deviceConnectedCallback(BLEStatus status, BLEDevice *device) {
  switch (status){
    case BLE_STATUS_OK:
      Serial.println("Device connected!");
      break;
    default:
      break;
  }
}

Device Disconnected Callback

If the connection to a device breaks, the device disconnected callback is called.

void deviceDisconnectedCallback(BLEDevice * device){
  Serial.println("Disconnected.");
}

Read Callback

In BTstack, the Read Callback is first called to query the size of the Charcteristic Value, before it is called to provide the data. Both times, the size has to be returned. The data is only stored in the provided buffer, if the buffer argeument is not NULL. If more than one dynamic Characteristics is used, the value handle is used to distinguish them.

uint16_t gattReadCallback(uint16_t value_handle, uint8_t * buffer, uint16_t buffer_size){
  if (buffer){
    Serial.print("gattReadCallback, value: ");
    Serial.println(characteristic_data, HEX);
    buffer[0] = characteristic_data;
  }
  return 1;
}

Write Callback

When the remove device writes a Characteristic Value, the Write callback is called. The buffer arguments points to the data of size size/ If more than one dynamic Characteristics is used, the value handle is used to distinguish them.

int gattWriteCallback(uint16_t value_handle, uint8_t *buffer, uint16_t size){
  characteristic_data = buffer[0];
  Serial.print("gattWriteCallback , value ");
  Serial.println(characteristic_data, HEX);
  return 0;
}