In this section, we will describe a number of examples from the example folder. Here is a list of existing examples:

Hello World - Blinking an LED without Bluetooth

Source Code: led_counter.c

The example demonstrates how to provide a periodic timer to toggle an LED and send debug messages to the console as a minimal BTstack test.

Periodic Timer Setup

As timers in BTstack are single shot, the periodic counter is implemented by re-registering the timer source in the heartbeat handler callback function. Listing here shows heartbeat handler adapted to periodically toggle an LED and print number of toggles.

static void heartbeat_handler(btstack_timer_source_t *ts){
  UNUSED(ts);

  // increment counter
  counter++;

  // pr164">int and log
  log_info("BTstack Counter %u",   counter);
  pr164">intf("BTstack counter %04u\n\r", counter);

  // toggle LED
  hal_led_toggle();

  // re-register timer
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);
}

Main Application Setup

Listing here shows main application code. It configures the heartbeat tier and adds it to the run loop.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  // set one-shot timer
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);

  pr164">intf("Running...\n\r");
  return 0;
}

GAP Classic Inquiry

Source Code: gap_inquiry.c

The Generic Access Profile (GAP) defines how Bluetooth devices discover and establish a connection with each other. In this example, the application discovers surrounding Bluetooth devices and collects their Class of Device (CoD), page scan mode, clock offset, and RSSI. After that, the remote name of each device is requested. In the following section we outline the Bluetooth logic part, i.e., how the packet handler handles the asynchronous events and data packets.

Bluetooth Logic

The Bluetooth logic is implemented as a state machine within the packet handler. In this example, the following states are passed sequentially: INIT, and ACTIVE.

In INIT, an inquiry scan is started, and the application transits to ACTIVE state.

In ACTIVE, the following events are processed:

For more details on discovering remote devices, please see Section on GAP.

Main Application Setup

Listing here shows main application code. It registers the HCI packet handler and starts the Bluetooth stack.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]) {
  (void)argc;
  (void)argv;

  // enabled EIR
  hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR);

  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // turn on!
  hci_power_control(HCI_POWER_ON);

  return 0;
}

Source Code: gap_link_keys.c

Shows how to iterate over the Classic Link Keys stored in NVS Link Keys are per device-device bonding. If the Bluetooth Controller can be swapped, e.g. on desktop systems, a Link Key DB for each Controller is needed. We need to wait until the Bluetooth Stack has started up and selected the correct Link Key DB based on the Controller's BD_ADDR.

List stored link keys

Bluetooth Logic

Wait for Bluetooth startup before listing the stored link keys

Main Application Setup

Listing here shows main application code. It registers the HCI packet handler and starts the Bluetooth stack.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]) {
  (void)argc;
  (void)argv;

  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // turn on!
  hci_power_control(HCI_POWER_ON);

  return 0;
}

GAP LE Advertisements Scanner

Source Code: gap_le_advertisements.c

This example shows how to scan and parse advertisements.

GAP LE setup for receiving advertisements

GAP LE advertisements are received as custom HCI events of the GAP_EVENT_ADVERTISING_REPORT type. To receive them, you'll need to register the HCI packet handler, as shown in Listing here.

static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);

static void gap_le_advertisements_setup(void){
  // Active scanning, 100% (scan 164">interval = scan window)
  gap_set_scan_parameters(1,48,48);
  gap_start_scan();

  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

GAP LE Advertising Data Dumper

Here, we use the definition of advertising data types and flags as specified in Assigned Numbers GAP and Supplement to the Bluetooth Core Specification, v4.

static const char * ad_types[] = {
  "", 
  "Flags",
  "Incomplete List of 16-bit Service Class UUIDs",
  "Complete List of 16-bit Service Class UUIDs",
  "Incomplete List of 32-bit Service Class UUIDs",
  "Complete List of 32-bit Service Class UUIDs",
  "Incomplete List of 128-bit Service Class UUIDs",
  "Complete List of 128-bit Service Class UUIDs",
  "Shortened Local Name",
  "Complete Local Name",
  "Tx Power Level",
  "", 
  "", 
  "Class of Device",
  "Simple Pairing Hash C",
  "Simple Pairing Randomizer R",
  "Device ID",
  "Security Manager TK Value",
  "Slave Connection Interval Range",
  "",
  "List of 16-bit Service Solicitation UUIDs",
  "List of 128-bit Service Solicitation UUIDs",
  "Service Data",
  "Public Target Address",
  "Random Target Address",
  "Appearance",
  "Advertising Interval"
};

static const char * flags[] = {
  "LE Limited Discoverable Mode",
  "LE General Discoverable Mode",
  "BR/EDR Not Supported",
  "Simultaneous LE and BR/EDR to Same Device Capable (Controller)",
  "Simultaneous LE and BR/EDR to Same Device Capable (Host)",
  "Reserved",
  "Reserved",
  "Reserved"
};

BTstack offers an iterator for parsing sequence of advertising data (AD) structures, see BLE advertisements parser API. After initializing the iterator, each AD structure is dumped according to its type.

static void dump_advertisement_data(const uint8_t * adv_data, uint8_t adv_size){
  ad_context_t context;
  bd_addr_t address;
  u164">int8_t uuid_128[16];
  for (ad_iterator_init(&context, adv_size, (u164">int8_t *)adv_data) ; ad_iterator_has_more(&context) ; ad_iterator_next(&context)){
    u164">int8_t data_type  = 0">ad_iterator_get_data_type(&context);
    u164">int8_t size     = 1">ad_iterator_get_data_len(&context);
    const u164">int8_t * data = ad_iterator_get_data(&context);

    if (data_type > 0 && data_type < 0x1B){
      pr164">intf("  %s: ", ad_types[data_type]);
    } 
    164">int i;
    // Assigned Numbers GAP

    switch (data_type){
      case BLUETOOTH_DATA_TYPE_FLAGS:
        // show only first octet, ignore rest
        for (i=0; i<8;i++){
          if (data[0] & (1<<i)){
            pr164">intf("%s; ", flags[i]);
          }

        }
        break;
      case BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS:
        for (i=0; i<size;i+=2){
          pr164">intf("%02X ", little_endian_read_16(data, i));
        }
        break;
      case BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS:
        for (i=0; i<size;i+=4){
          pr164">intf("%04"PRIX32, little_endian_read_32(data, i));
        }
        break;
      case BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS:
      case BLUETOOTH_DATA_TYPE_LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS:
        reverse_128(data, uuid_128);
        pr164">intf("%s", uuid128_to_str(uuid_128));
        break;
      case BLUETOOTH_DATA_TYPE_SHORTENED_LOCAL_NAME:
      case BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME:
        for (i=0; i<size;i++){
          pr164">intf("%c", (char)(data[i]));
        }
        break;
      case BLUETOOTH_DATA_TYPE_TX_POWER_LEVEL:
        pr164">intf("%d dBm", *(164">int8_t*)data);
        break;
      case BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE:
        pr164">intf("Connection Interval Min = %u ms, Max = %u ms", little_endian_read_16(data, 0) * 5/4, little_endian_read_16(data, 2) * 5/4);
        break;
      case BLUETOOTH_DATA_TYPE_SERVICE_DATA:
        pr164">intf_hexdump(data, size);
        break;
      case BLUETOOTH_DATA_TYPE_PUBLIC_TARGET_ADDRESS:
      case BLUETOOTH_DATA_TYPE_RANDOM_TARGET_ADDRESS:
        reverse_bd_addr(data, address);
        pr164">intf("%s", bd_addr_to_str(address));
        break;
      case BLUETOOTH_DATA_TYPE_APPEARANCE: 
        // https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.gap.appearance.xml
        pr164">intf("%02X", little_endian_read_16(data, 0) );
        break;
      case BLUETOOTH_DATA_TYPE_ADVERTISING_INTERVAL:
        pr164">intf("%u ms", little_endian_read_16(data, 0) * 5/8 );
        break;
      case BLUETOOTH_DATA_TYPE_3D_INFORMATION_DATA:
        pr164">intf_hexdump(data, size);
        break;
      case BLUETOOTH_DATA_TYPE_MANUFACTURER_SPECIFIC_DATA: // Manufacturer Specific Data 
        break;
      case BLUETOOTH_DATA_TYPE_CLASS_OF_DEVICE:
      case BLUETOOTH_DATA_TYPE_SIMPLE_PAIRING_HASH_C:
      case BLUETOOTH_DATA_TYPE_SIMPLE_PAIRING_RANDOMIZER_R:
      case BLUETOOTH_DATA_TYPE_DEVICE_ID: 
      case BLUETOOTH_DATA_TYPE_SECURITY_MANAGER_OUT_OF_BAND_FLAGS:
      default:
        pr164">intf("Advertising Data Type 0x%2x not handled yet", data_type); 
        break;
    }    
    pr164">intf("\n");
  }
  pr164">intf("\n");
}

HCI packet handler

The HCI packet handler has to start the scanning, and to handle received advertisements. Advertisements are received as HCI event packets of the GAP_EVENT_ADVERTISING_REPORT type, see Listing here.

static void packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  bd_addr_t address;
  u164">int8_t address_type;
  u164">int8_t event_type;
  164">int8_t rssi;
  u164">int8_t length;
  const u164">int8_t * data;

  switch (hci_event_packet_get_type(packet)) {
    case GAP_EVENT_ADVERTISING_REPORT:
      gap_event_advertising_report_get_address(packet, address);
      event_type = gap_event_advertising_report_get_advertising_event_type(packet);
      address_type = gap_event_advertising_report_get_address_type(packet);
      rssi = gap_event_advertising_report_get_rssi(packet);
      length = gap_event_advertising_report_get_data_length(packet);
      data = gap_event_advertising_report_get_data(packet);
      pr164">intf("Advertisement (legacy) event: evt-type %u, addr-type %u, addr %s, rssi %d, data[%u] ", event_type,
         address_type, bd_addr_to_str(address), rssi, length);
      pr164">intf_hexdump(data, length);
      dump_advertisement_data(data, length);
      break;
#ifdef ENABLE_LE_EXTENDED_ADVERTISING
    case GAP_EVENT_EXTENDED_ADVERTISING_REPORT:
      gap_event_extended_advertising_report_get_address(packet, address);
      event_type = gap_event_extended_advertising_report_get_advertising_event_type(packet);
      address_type = gap_event_extended_advertising_report_get_address_type(packet);
      rssi = gap_event_extended_advertising_report_get_rssi(packet);
      length = gap_event_extended_advertising_report_get_data_length(packet);
      data = gap_event_extended_advertising_report_get_data(packet);
      pr164">intf("Advertisement (extended) event: evt-type %u, addr-type %u, addr %s, rssi %d, data[%u] ", event_type,
         address_type, bd_addr_to_str(address), rssi, length);
      pr164">intf_hexdump(data, length);
      dump_advertisement_data(data, length);
      break;
#endif
    default:
      break;
  }
}

GATT Client - Discover Primary Services

Source Code: gatt_browser.c

This example shows how to use the GATT Client API to discover primary services and their characteristics of the first found device that is advertising its services.

The logic is divided between the HCI and GATT client packet handlers. The HCI packet handler is responsible for finding a remote device, connecting to it, and for starting the first GATT client query. Then, the GATT client packet handler receives all primary services and requests the characteristics of the last one to keep the example short.

GATT client setup

In the setup phase, a GATT client must register the HCI and GATT client packet handlers, as shown in Listing here. Additionally, the security manager can be setup, if signed writes, or encrypted, or authenticated connection are required, to access the characteristics, as explained in Section on SMP.

// Handles connect, disconnect, and advertising report events,  
// starts the GATT client, and sends the first query.
static void handle_hci_event(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

// Handles GATT client query results, sends queries and the 
// GAP disconnect command when the querying is done.
static void handle_gatt_client_event(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void gatt_client_setup(void){

  // Initialize L2CAP and register HCI event handler
  l2cap_init();

  // Initialize GATT client 
  gatt_client_init();

  // Optinoally, Setup security manager
  sm_init();
  sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);

  // register for HCI events
  hci_event_callback_registration.callback = &handle_hci_event;
  hci_add_event_handler(&hci_event_callback_registration);
}

HCI packet handler

The HCI packet handler has to start the scanning, to find the first advertising device, to stop scanning, to connect to and later to disconnect from it, to start the GATT client upon the connection is completed, and to send the first query - in this case the gatt_client_discover_primary_services() is called, see Listing here.

static void handle_hci_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;
  advertising_report_t report;

  u164">int8_t event = hci_event_packet_get_type(packet);
  switch (event) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) break;
      if (cmdline_addr_found){
        pr164">intf("Trying to connect to %s\n", bd_addr_to_str(cmdline_addr));
        gap_connect(cmdline_addr, 0);
        break;
      }
      pr164">intf("BTstack activated, start scanning!\n");
      gap_set_scan_parameters(0,0x0030, 0x0030);
      gap_start_scan();
      break;
    case GAP_EVENT_ADVERTISING_REPORT:
      fill_advertising_report_from_packet(&report, packet);
      dump_advertising_report(&report);

      // stop scanning, and connect to the device
      gap_stop_scan();
      gap_connect(report.address,report.address_type);
      break;
    case HCI_EVENT_META_GAP:
      // wait for connection complete
      if (hci_event_gap_meta_get_subevent_code(packet) !=  GAP_SUBEVENT_LE_CONNECTION_COMPLETE) break;
      pr164">intf("\nGATT browser - CONNECTED\n");
      connection_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
      // query primary services
      gatt_client_discover_primary_services(handle_gatt_client_event, connection_handle);
      break;
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      pr164">intf("\nGATT browser - DISCONNECTED\n");
      break;
    default:
      break;
  }
}

GATT Client event handler

Query results and further queries are handled by the GATT client packet handler, as shown in Listing here. Here, upon receiving the primary services, the gatt_client_discover_characteristics_for_service() query for the last received service is sent. After receiving the characteristics for the service, gap_disconnect is called to terminate the connection. Upon disconnect, the HCI packet handler receives the disconnect complete event.

static void handle_gatt_client_event(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(packet_type);
  UNUSED(channel);
  UNUSED(size);

  gatt_client_service_t service;
  gatt_client_characteristic_t characteristic;
  switch(hci_event_packet_get_type(packet)){
    case GATT_EVENT_SERVICE_QUERY_RESULT:
      gatt_event_service_query_result_get_service(packet, &service);
      dump_service(&service);
      services[service_count++] = service;
      break;
    case GATT_EVENT_CHARACTERISTIC_QUERY_RESULT:
      gatt_event_characteristic_query_result_get_characteristic(packet, &characteristic);
      dump_characteristic(&characteristic);
      break;
    case GATT_EVENT_QUERY_COMPLETE:
      // GATT_EVENT_QUERY_COMPLETE of search characteristics
      if (service_index < service_count) {
        service = services[service_index++];
        pr164">intf("\nGATT browser - CHARACTERISTIC for SERVICE %s, [0x%04x-0x%04x]\n",
          uuid128_to_str(service.uuid128), service.start_group_handle, service.end_group_handle);
        gatt_client_discover_characteristics_for_service(handle_gatt_client_event, connection_handle, &service);
        break;
      }
      service_index = 0;
      break;
    default:
      break;
  }
}

GATT Server - Heartbeat Counter over GATT

Source Code: gatt_counter.c

All newer operating systems provide GATT Client functionality. The LE Counter examples demonstrates how to specify a minimal GATT Database with a custom GATT Service and a custom Characteristic that sends periodic notifications.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager and configures the ATT Server with the pre-compiled ATT Database generated from $le_counter.gatt$. Additionally, it enables the Battery Service Server with the current battery level. Finally, it configures the advertisements and the heartbeat handler and boots the Bluetooth stack. In this example, the Advertisement contains the Flags attribute and the device name. The flag 0x06 indicates: LE General Discoverable Mode and BR/EDR not supported.

static int  le_notification_enabled;
static btstack_timer_source_t heartbeat;
static btstack_packet_callback_registration_t hci_event_callback_registration;
static hci_con_handle_t con_handle;
static u164">int8_t battery = 100;

#ifdef ENABLE_GATT_OVER_CLASSIC
static u164">int8_t gatt_service_buffer[70];
#endif

static void packet_handler (u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static 144">u164">int16_t att_read_callback(hci_con_handle_t con_handle, 144">u164">int16_t att_handle, 144">u164">int16_t offset, u164">int8_t * buffer, 144">u164">int16_t buffer_size);
static 164">int att_write_callback(hci_con_handle_t con_handle, 144">u164">int16_t att_handle, 144">u164">int16_t transaction_mode, 144">u164">int16_t offset, u164">int8_t *buffer, 144">u164">int16_t buffer_size);
static void  heartbeat_handler(struct btstack_timer_source *ts);
static void beat(void);

// Flags general discoverable, BR/EDR supported (== not supported flag not set) when ENABLE_GATT_OVER_CLASSIC is enabled
#ifdef ENABLE_GATT_OVER_CLASSIC
#define APP_AD_FLAGS 0x02
#else
#define APP_AD_FLAGS 0x06
#endif

const u164">int8_t adv_data[] = {
  // Flags general discoverable
  0x02, BLUETOOTH_DATA_TYPE_FLAGS, APP_AD_FLAGS,
  // Name
  0x0b, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'L', 'E', ' ', 'C', 'o', 'u', 'n', 't', 'e', 'r', 
  // Incomplete List of 16-bit Service Class UUIDs -- FF10 - only valid for testing!
  0x03, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0x10, 0xff,
};
const u164">int8_t adv_data_len = sizeof(adv_data);

static void le_counter_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

#ifdef ENABLE_GATT_OVER_CLASSIC
  // init SDP, create record for GATT and register with SDP
  sdp_init();
  memset(gatt_service_buffer, 0, sizeof(gatt_service_buffer));
  gatt_create_sdp_record(gatt_service_buffer, sdp_create_service_record_handle(), ATT_SERVICE_GATT_SERVICE_START_HANDLE, ATT_SERVICE_GATT_SERVICE_END_HANDLE);
  btstack_assert(de_get_len( gatt_service_buffer) <= sizeof(gatt_service_buffer));
  sdp_register_service(gatt_service_buffer);

  // configure Classic GAP
  gap_set_local_name("GATT Counter BR/EDR 00:00:00:00:00:00");
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_discoverable_control(1);
#endif

  // setup ATT server
  att_server_init(profile_data, att_read_callback, att_write_callback);

  // setup battery service
  battery_service_server_init(battery);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for ATT event
  1">att_server_register_packet_handler(packet_handler);

  // set one-shot timer
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);

  // beat once
  beat();
}

Heartbeat Handler

The heartbeat handler updates the value of the single Characteristic provided in this example, and request a ATT_EVENT_CAN_SEND_NOW to send a notification if enabled see Listing here.

static int  counter = 0;
static char counter_string[30];
static 164">int  counter_string_len;

static void beat(void){
  counter++;
  counter_string_len = snpr164">intf(counter_string, sizeof(counter_string), "BTstack counter %04u", counter);
  puts(counter_string);
}

static void heartbeat_handler(struct btstack_timer_source *ts){
  if (le_notification_enabled) {
    beat();
    186">att_server_request_can_send_now_event(con_handle);
  }

  // simulate battery drain
  battery--;
  if (battery < 50) {
    battery = 100;
  }
  battery_service_server_set_battery_value(battery);

  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

Packet Handler

The packet handler is used to:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  switch (hci_event_packet_get_type(packet)) {
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      le_notification_enabled = 0;
      break;
    case ATT_EVENT_CAN_SEND_NOW:
      131">att_server_notify(con_handle, ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE, (u164">int8_t*) counter_string, counter_string_len);
      break;
    default:
      break;
  }
}

ATT Read

The ATT Server handles all reads to constant data. For dynamic data like the custom characteristic, the registered att_read_callback is called. To handle long characteristics and long reads, the att_read_callback is first called with buffer == NULL, to request the total value length. Then it will be called again requesting a chunk of the value. See Listing here.

// ATT Client Read Callback for Dynamic Data
// - if buffer == NULL, don't copy data, just return size of value
// - if buffer != NULL, copy data and return number bytes copied
// @param offset defines start of attribute value
static 144">u164">int16_t att_read_callback(hci_con_handle_t connection_handle, 144">u164">int16_t att_handle, 144">u164">int16_t offset, u164">int8_t * buffer, 144">u164">int16_t buffer_size){
  UNUSED(connection_handle);

  if (att_handle == ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE){
    return att_read_callback_handle_blob((const u164">int8_t *)counter_string, counter_string_len, offset, buffer, buffer_size);
  }
  return 0;
}

ATT Write

The only valid ATT writes in this example are to the Client Characteristic Configuration, which configures notification and indication and to the the Characteristic Value. If the ATT handle matches the client configuration handle, the new configuration value is stored and used in the heartbeat handler to decide if a new value should be sent. If the ATT handle matches the characteristic value handle, we print the write as hexdump See Listing here.

static int att_write_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
  switch (att_handle){
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
      le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
      con_handle = connection_handle;
      break;
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
      pr164">intf("Write: transaction mode %u, offset %u, data (%u bytes): ", transaction_mode, offset, buffer_size);
      pr164">intf_hexdump(buffer, buffer_size);
      break;
    default:
      break;
  }
  return 0;
}

Performance - Stream Data over GATT (Server)

Source Code: gatt_streamer_server.c

All newer operating systems provide GATT Client functionality. This example shows how to get a maximal throughput via BLE:

In theory, we should also update the connection parameters, but we already get a connection interval of 30 ms and there's no public way to use a shorter interval with iOS (if we're not implementing an HID device).

Note: To start the streaming, run the example. On remote device use some GATT Explorer, e.g. LightBlue, BLExplr to enable notifications.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager, and configures the ATT Server with the pre-compiled ATT Database generated from $le_streamer.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack.

static void le_streamer_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

#ifdef ENABLE_GATT_OVER_CLASSIC
  // init SDP, create record for GATT and register with SDP
  sdp_init();
  memset(gatt_service_buffer, 0, sizeof(gatt_service_buffer));
  gatt_create_sdp_record(gatt_service_buffer, 0x10001, ATT_SERVICE_GATT_SERVICE_START_HANDLE, ATT_SERVICE_GATT_SERVICE_END_HANDLE);
  sdp_register_service(gatt_service_buffer);
  pr164">intf("SDP service record size: %u\n", de_get_len(gatt_service_buffer));

  // configure Classic GAP
  gap_set_local_name("GATT Streamer BR/EDR 00:00:00:00:00:00");
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_discoverable_control(1);
#endif

  // setup ATT server
  att_server_init(profile_data, NULL, att_write_callback);

  // register for HCI events
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for ATT events
  1">att_server_register_packet_handler(att_packet_handler);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // init client state
  init_connections();
}

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(le_streamer_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_sent(le_streamer_connection_t * context, 164">int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

HCI Packet Handler

The packet handler is used track incoming connections and to stop notifications on disconnect It is also a good place to request the connection parameter update as indicated in the commented code block.

static void hci_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  144">u164">int16_t conn_164">interval;
  hci_con_handle_t con_handle;
  static const char * const phy_names[] = {
    "1 M", "2 M", "Codec"
  };

  switch (hci_event_packet_get_type(packet)) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
        pr164">intf("To start the streaming, please run the le_streamer_client example on other device, or use some GATT Explorer, e.g. LightBlue, BLExplr.\n");
      } 
      break;
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      con_handle = hci_event_disconnection_complete_get_connection_handle(packet);
      pr164">intf("- LE Connection 0x%04x: disconnect, reason %02x\n", con_handle, hci_event_disconnection_complete_get_reason(packet));          
      break;
    case HCI_EVENT_META_GAP:
      switch (hci_event_gap_meta_get_subevent_code(packet)) {
        case GAP_SUBEVENT_LE_CONNECTION_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = gap_subevent_le_connection_complete_get_connection_handle(packet);
          conn_164">interval = gap_subevent_le_connection_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connected - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), gap_subevent_le_connection_complete_get_conn_latency(packet));

          // request min con 164">interval 15 ms for iOS 11+
          pr164">intf("- LE Connection 0x%04x: request 15 ms connection 164">interval\n", con_handle);
          gap_request_connection_parameter_update(con_handle, 12, 12, 4, 0x0048);
          break;
        default:
          break;
      }
      break;
    case HCI_EVENT_LE_META:
      switch (hci_event_le_meta_get_subevent_code(packet)) {
        case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = hci_subevent_le_connection_update_complete_get_connection_handle(packet);
          conn_164">interval = hci_subevent_le_connection_update_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connection update - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));
          break;
        case HCI_SUBEVENT_LE_DATA_LENGTH_CHANGE:
          con_handle = hci_subevent_le_data_length_change_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: data length change - max %u bytes per packet\n", con_handle,
               hci_subevent_le_data_length_change_get_max_tx_octets(packet));
          break;
        case HCI_SUBEVENT_LE_PHY_UPDATE_COMPLETE:
          con_handle = hci_subevent_le_phy_update_complete_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: PHY update - using LE %s PHY now\n", con_handle,
               phy_names[hci_subevent_le_phy_update_complete_get_tx_phy(packet)]);
          break;
        default:
          break;
      }
      break;

    default:
      break;
  }
}

ATT Packet Handler

The packet handler is used to track the ATT MTU Exchange and trigger ATT send

static void att_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  164">int mtu;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        case ATT_EVENT_CONNECTED:
          // setup new 
          context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
          if (!context) break;
          context->counter = 'A';
          context->connection_handle = att_event_connected_get_handle(packet);
          context->test_data_len = btstack_min(103">att_server_get_mtu(context->connection_handle) - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT connected, handle 0x%04x, test data len %u\n", context->name, context->connection_handle, context->test_data_len);
          break;
        case ATT_EVENT_MTU_EXCHANGE_COMPLETE:
          mtu = att_event_mtu_exchange_complete_get_MTU(packet) - 3;
          context = connection_for_conn_handle(att_event_mtu_exchange_complete_get_handle(packet));
          if (!context) break;
          context->test_data_len = btstack_min(mtu - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT MTU = %u => use test data of len %u\n", context->name, mtu, context->test_data_len);
          break;
        case ATT_EVENT_CAN_SEND_NOW:
          streamer();
          break;
        case ATT_EVENT_DISCONNECTED:
          context = connection_for_conn_handle(att_event_disconnected_get_handle(packet));
          if (!context) break;
          // free connection
          pr164">intf("%c: ATT disconnected, handle 0x%04x\n", context->name, context->connection_handle);          
          context->le_notification_enabled = 0;
          context->connection_handle = HCI_CON_HANDLE_INVALID;
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  // find next active streaming connection
  164">int old_connection_index = connection_index;
  while (1){
    // active found?
    if ((le_streamer_connections[connection_index].connection_handle != HCI_CON_HANDLE_INVALID) &&
      (le_streamer_connections[connection_index].le_notification_enabled)) break;

    // check next
    next_connection_index();

    // none found
    if (connection_index == old_connection_index) return;
  }

  le_streamer_connection_t * context = &le_streamer_connections[connection_index];

  // create test data
  context->counter++;
  if (context->counter > 'Z') context->counter = 'A';
  memset(context->test_data, context->counter, context->test_data_len);

  // send
  131">att_server_notify(context->connection_handle, context->value_handle, (u164">int8_t*) context->test_data, context->test_data_len);

  // track
  test_track_sent(context, context->test_data_len);

  // request next send event
  186">att_server_request_can_send_now_event(context->connection_handle);

  // check next
  next_connection_index();
}

ATT Write

The only valid ATT write in this example is to the Client Characteristic Configuration, which configures notification and indication. If the ATT handle matches the client configuration handle, the new configuration value is stored. If notifications get enabled, an ATT_EVENT_CAN_SEND_NOW is requested. See Listing here.

static int att_write_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
  UNUSED(offset);

  // pr164">intf("att_write_callback att_handle 0x%04x, transaction mode %u\n", att_handle, transaction_mode);
  if (transaction_mode != ATT_TRANSACTION_MODE_NONE) return 0;
  le_streamer_connection_t * context = connection_for_conn_handle(con_handle);
  switch(att_handle){
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
      context->le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
      pr164">intf("%c: Notifications enabled %u\n", context->name, context->le_notification_enabled); 
      if (context->le_notification_enabled){
        switch (att_handle){
          case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          default:
            break;
        }
        186">att_server_request_can_send_now_event(context->connection_handle);
      }
      test_reset(context);
      break;
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
      test_track_sent(context, buffer_size);
      break;
    default:
      pr164">intf("Write to 0x%04x, len %u\n", att_handle, buffer_size);
      break;
  }
  return 0;
}

GATT Battery Service Client

Source Code: gatt_battery_query.c

This example demonstrates how to use the GATT Battery Service client to receive battery level information. The client supports querying of multiple battery services instances of on the remote device. The example scans for remote devices and connects to the first found device and starts the battery service client.

Main Application Setup

The Listing here shows how to setup Battery Service client. Besides calling init() method for each service, you'll also need to register HCI packet handler to handle advertisements, as well as connect and disconect events.

Handling of GATT Battery Service events will be later delegated to a sepparate packet handler, i.e. gatt_client_event_handler.

@note There are two additional files associated with this client to allow a remote device to query out GATT database:

static void hci_event_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void gatt_client_event_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void battery_service_client_setup(void){
  // Init L2CAP
  l2cap_init();

  // Setup ATT server - only needed if LE Peripheral does ATT queries on its own, e.g. Android phones
  att_server_init(profile_data, NULL, NULL);

  // GATT Client setup
  gatt_client_init();
  // Device Information Service Client setup
  138">battery_service_client_init();

  sm_init();
  sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);

  hci_event_callback_registration.callback = &hci_event_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

static void hci_event_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
...
  bd_addr_t address;

  if (packet_type != HCI_EVENT_PACKET){
    return;  
  }

  switch (hci_event_packet_get_type(packet)) {
...
    case HCI_EVENT_META_GAP:
      // Wait for connection complete
      if (hci_event_gap_meta_get_subevent_code(packet) !=  GAP_SUBEVENT_LE_CONNECTION_COMPLETE) break;

...
      // Get connection handle from event
      connection_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);

      // Connect to remote Battery Service. 
      // On succesful connection, the client tries to register for notifications. If notifications 
      // are not supported by remote Battery Service, the client will automatically poll the battery 165">level - here every 2 seconds.
      // If poll_164">interval_ms is 0, polling is disabled, and only notifications will be received (for manual polling, 
      // see battery_service_client.h).
      // All GATT Battery Service events are handled by the gatt_client_event_handler.
      (void) battery_service_client_connect(connection_handle, gatt_client_event_handler, 2000, &battery_service_cid);

      app_state = APP_STATE_CONNECTED;
      pr164">intf("Battery service connected.\n");
      break;

    case HCI_EVENT_DISCONNECTION_COMPLETE:
      connection_handle = HCI_CON_HANDLE_INVALID;
      // Disconnect battery service
      178">battery_service_client_disconnect(battery_service_cid);

...
      pr164">intf("Disconnected %s\n", bd_addr_to_str(report.address));
      pr164">intf("Restart scan.\n");
      app_state = APP_STATE_W4_SCAN_RESULT;
      gap_start_scan();
      break;
    default:
      break;
  }
}

// The gatt_client_event_handler receives following events from remote device:
//  - GATTSERVICE_SUBEVENT_BATTERY_SERVICE_CONNECTED
//  - GATTSERVICE_SUBEVENT_BATTERY_SERVICE_LEVEL   
// 
static void gatt_client_event_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size){
...
  u164">int8_t status;
  u164">int8_t att_status;

  if (hci_event_packet_get_type(packet) != HCI_EVENT_GATTSERVICE_META){
    return;
  }

  switch (hci_event_gattservice_meta_get_subevent_code(packet)){
    case GATTSERVICE_SUBEVENT_BATTERY_SERVICE_CONNECTED:
      status = gattservice_subevent_battery_service_connected_get_status(packet);
      switch (status){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Battery service client connected, found %d services, poll bitmap 0x%02x\n", 
            gattservice_subevent_battery_service_connected_get_num_instances(packet),
            gattservice_subevent_battery_service_connected_get_poll_bitmap(packet));
            battery_service_client_read_battery_165">level(battery_service_cid, 0);
          break;
        default:
          pr164">intf("Battery service client connection failed, status 0x%02x.\n", status);
          add_to_blacklist(report.address);
          gap_disconnect(connection_handle);
          break;
      }
      break;

    case GATTSERVICE_SUBEVENT_BATTERY_SERVICE_LEVEL:
      att_status = gattservice_subevent_battery_service_165">level_get_att_status(packet);
      if (att_status != ATT_ERROR_SUCCESS){
        pr164">intf("Battery 165">level read failed, ATT Error 0x%02x\n", att_status);
      } else {
        pr164">intf("Service index: %d, Battery 165">level: %d\n", 
          gattservice_subevent_battery_service_165">level_get_sevice_index(packet), 
          gattservice_subevent_battery_service_165">level_get_165">level(packet));

      }
      break;

    default:
      break;
  }
}

GATT Device Information Service Client

Source Code: gatt_device_information_query.c

This example demonstrates how to use the GATT Device Information Service client to receive device information such as various IDs and revisions. The example scans for remote devices and connects to the first found device. If the remote device provides a Device Information Service, the information is collected and printed in console output, otherwise, the device will be blacklisted and the scan restarted.

Main Application Setup

The Listing here shows how to setup Device Information Service client. Besides calling init() method for each service, you'll also need to register HCI packet handler to handle advertisements, as well as connect and disconect events.

Handling of GATT Device Information Service events will be later delegated to a sepparate packet handler, i.e. gatt_client_event_handler.

@note There are two additional files associated with this client to allow a remote device to query out GATT database:

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void gatt_client_event_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void device_information_service_client_setup(void){
  // Init L2CAP
  l2cap_init();

  // Setup ATT server - only needed if LE Peripheral does ATT queries on its own, e.g. Android phones
  att_server_init(profile_data, NULL, NULL);

  // GATT Client setup
  gatt_client_init();
  // Device Information Service Client setup
  device_information_service_client_init();

  sm_init();
  sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);

  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
...
    case HCI_EVENT_META_GAP:
      // wait for connection complete
      if (hci_event_gap_meta_get_subevent_code(packet) !=  GAP_SUBEVENT_LE_CONNECTION_COMPLETE) break;

...
      // get connection handle from event
      connection_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);

      // Connect to remote Device Information Service. The client will query the remote service and emit events,
      // that will be passed on to gatt_client_event_handler.
      status = device_information_service_client_query(connection_handle, gatt_client_event_handler);
      btstack_assert(status == ERROR_CODE_SUCCESS);

      pr164">intf("Device Information connected.\n");

      app_state = APP_STATE_CONNECTED;
      break;
...
  u164">int8_t att_status = 0;

  if (hci_event_packet_get_type(packet) != HCI_EVENT_GATTSERVICE_META){
    return;
  }

  switch (hci_event_gattservice_meta_get_subevent_code(packet)){
    case GATTSERVICE_SUBEVENT_DEVICE_INFORMATION_MANUFACTURER_NAME:
      att_status = gattservice_subevent_device_information_manufacturer_name_get_att_status(packet);
      if (att_status != ATT_ERROR_SUCCESS){
        pr164">intf("Manufacturer Name read failed, ATT Error 0x%02x\n", att_status);
      } else {
        pr164">intf("Manufacturer Name: %s\n", gattservice_subevent_device_information_manufacturer_name_get_value(packet));
      }
      break;

    // ...
...
    case GATTSERVICE_SUBEVENT_DEVICE_INFORMATION_DONE:
      att_status = gattservice_subevent_device_information_serial_number_get_att_status(packet);
      switch (att_status){
        case ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE:
          pr164">intf("Device Information service client not found.\n");
          add_to_blacklist(report.address);
          gap_disconnect(connection_handle);
          break;
        case ATT_ERROR_SUCCESS:
          pr164">intf("Query done\n");
          break;
        default:
          pr164">intf("Query failed, ATT Error 0x%02x\n", att_status);
          break;

      }
      if (att_status != ATT_ERROR_SUCCESS){
        if (att_status == ERROR_CODE_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE)
        pr164">intf("Query failed, ATT Error 0x%02x\n", att_status);
      } else {
        pr164">intf("Query done\n");
      }
      break;
    default:
      break;
  }
}

GATT Heart Rate Sensor Client

Source Code: gatt_heart_rate_client.c

Connects for Heart Rate Sensor and reports measurements.

LE Nordic SPP-like Heartbeat Server

Source Code: nordic_ssp_le_counter.c

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager and configures the ATT Server with the pre-compiled ATT Database generated from $nordic_ssp_le_counter.gatt$. Additionally, it enables the Battery Service Server with the current battery level. Finally, it configures the advertisements and the heartbeat handler and boots the Bluetooth stack. In this example, the Advertisement contains the Flags attribute and the device name. The flag 0x06 indicates: LE General Discoverable Mode and BR/EDR not supported.

static btstack_timer_source_t heartbeat;
static hci_con_handle_t con_handle = HCI_CON_HANDLE_INVALID;
static btstack_context_callback_registration_t send_request;
static btstack_packet_callback_registration_t  hci_event_callback_registration;

const u164">int8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  2, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 
  // Name
  8, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'n', 'R', 'F',' ', 'S', 'P', 'P',
  // UUID ...
  17, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, 0x9e, 0xca, 0xdc, 0x24, 0xe, 0xe5, 0xa9, 0xe0, 0x93, 0xf3, 0xa3, 0xb5, 0x1, 0x0, 0x40, 0x6e,
};
const u164">int8_t adv_data_len = sizeof(adv_data);

Heartbeat Handler

The heartbeat handler updates the value of the single Characteristic provided in this example, and request a ATT_EVENT_CAN_SEND_NOW to send a notification if enabled see Listing here.

static int  counter = 0;
static char counter_string[30];
static 164">int  counter_string_len;

static void beat(void){
  counter++;
  counter_string_len = snpr164">intf(counter_string, sizeof(counter_string), "BTstack counter %03u", counter);
}

static void nordic_can_send(void * context){
  UNUSED(context);
  pr164">intf("SEND: %s\n", counter_string);
  nordic_spp_service_server.h#L82">nordic_spp_service_server_send(con_handle, (u164">int8_t*) counter_string, counter_string_len);
}

static void heartbeat_handler(struct btstack_timer_source *ts){
  if (con_handle != HCI_CON_HANDLE_INVALID) {
    beat();
    send_request.callback = &nordic_can_send;
    nordic_spp_service_server.h#L74">nordic_spp_service_server_request_can_send_now(&send_request, con_handle);
  }
  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

Packet Handler

The packet handler is used to:

static void hci_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  switch (hci_event_packet_get_type(packet)) {
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      con_handle = HCI_CON_HANDLE_INVALID;
      break;
    default:
      break;
  }
}

LE Nordic SPP-like Streamer Server

Source Code: nordic_spp_le_streamer.c

All newer operating systems provide GATT Client functionality. This example shows how to get a maximal throughput via BLE:

In theory, we should also update the connection parameters, but we already get a connection interval of 30 ms and there's no public way to use a shorter interval with iOS (if we're not implementing an HID device).

Note: To start the streaming, run the example. On remote device use some GATT Explorer, e.g. LightBlue, BLExplr to enable notifications.

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(nordic_spp_le_streamer_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_sent(nordic_spp_le_streamer_connection_t * context, 164">int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

HCI Packet Handler

The packet handler prints the welcome message and requests a connection paramter update for LE Connections

static void hci_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  144">u164">int16_t conn_164">interval;
  hci_con_handle_t con_handle;

  if (packet_type != HCI_EVENT_PACKET) return;

  switch (hci_event_packet_get_type(packet)) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
        pr164">intf("To start the streaming, please run nRF Toolbox -> UART to connect.\n");
      } 
      break;
    case HCI_EVENT_META_GAP:
      switch (hci_event_gap_meta_get_subevent_code(packet)) {
        case GAP_SUBEVENT_LE_CONNECTION_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = gap_subevent_le_connection_complete_get_connection_handle(packet);
          conn_164">interval = gap_subevent_le_connection_complete_get_conn_164">interval(packet);
          pr164">intf("LE Connection - Connection Interval: %u.%02u ms\n", conn_164">interval * 125 / 100, 25 * (conn_164">interval & 3));
          pr164">intf("LE Connection - Connection Latency: %u\n", gap_subevent_le_connection_complete_get_conn_latency(packet));

          // request min con 164">interval 15 ms for iOS 11+
          pr164">intf("LE Connection - Request 15 ms connection 164">interval\n");
          gap_request_connection_parameter_update(con_handle, 12, 12, 4, 0x0048);
          break;
        default:
          break;
      }
      break;

    case HCI_EVENT_LE_META:
      switch (hci_event_le_meta_get_subevent_code(packet)) {
        case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = hci_subevent_le_connection_update_complete_get_connection_handle(packet);
          conn_164">interval = hci_subevent_le_connection_update_complete_get_conn_164">interval(packet);
          pr164">intf("LE Connection - Connection Param update - connection 164">interval %u.%02u ms, latency %u\n", conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));
          break;
        default:
          break;
      }
      break;  
    default:
      break;
  }
}

ATT Packet Handler

The packet handler is used to setup and tear down the spp-over-gatt connection and its MTU

static void att_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  164">int mtu;
  nordic_spp_le_streamer_connection_t * context;

  switch (hci_event_packet_get_type(packet)) {
    case ATT_EVENT_CONNECTED:
      // setup new 
      context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
      if (!context) break;
      context->counter = 'A';
      context->test_data_len = ATT_DEFAULT_MTU - 4;   // -1 for nordic 0x01 packet type
      context->connection_handle = att_event_connected_get_handle(packet);
      break;
    case ATT_EVENT_MTU_EXCHANGE_COMPLETE:
      mtu = att_event_mtu_exchange_complete_get_MTU(packet) - 3;
      context = connection_for_conn_handle(att_event_mtu_exchange_complete_get_handle(packet));
      if (!context) break;
      context->test_data_len = btstack_min(mtu - 3, sizeof(context->test_data));
      pr164">intf("%c: ATT MTU = %u => use test data of len %u\n", context->name, mtu, context->test_data_len);
      break;
    case ATT_EVENT_DISCONNECTED:
      context = connection_for_conn_handle(att_event_disconnected_get_handle(packet));
      if (!context) break;
      // free connection
      pr164">intf("%c: Disconnect\n", context->name);          
      context->le_notification_enabled = 0;
      context->connection_handle = HCI_CON_HANDLE_INVALID;
      break;
    default:
      break;
  }
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void nordic_can_send(void * some_context){
  UNUSED(some_context);

  // find next active streaming connection
  164">int old_connection_index = connection_index;
  while (1){
    // active found?
    if ((nordic_spp_le_streamer_connections[connection_index].connection_handle != HCI_CON_HANDLE_INVALID) &&
      (nordic_spp_le_streamer_connections[connection_index].le_notification_enabled)) break;

    // check next
    next_connection_index();

    // none found
    if (connection_index == old_connection_index) return;
  }

  nordic_spp_le_streamer_connection_t * context = &nordic_spp_le_streamer_connections[connection_index];

  // create test data
  context->counter++;
  if (context->counter > 'Z') context->counter = 'A';
  memset(context->test_data, context->counter, context->test_data_len);

  // send
  nordic_spp_service_server.h#L82">nordic_spp_service_server_send(context->connection_handle, (u164">int8_t*) context->test_data, context->test_data_len);

  // track
  test_track_sent(context, context->test_data_len);

  // request next send event
  nordic_spp_service_server.h#L74">nordic_spp_service_server_request_can_send_now(&context->send_request, context->connection_handle);

  // check next
  next_connection_index();
}

LE u-blox SPP-like Heartbeat Server

Source Code: ublox_spp_le_counter.c

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager and configures the ATT Server with the pre-compiled ATT Database generated from $ublox_le_counter.gatt$. Additionally, it enables the Battery Service Server with the current battery level. Finally, it configures the advertisements and the heartbeat handler and boots the Bluetooth stack. In this example, the Advertisement contains the Flags attribute and the device name. The flag 0x06 indicates: LE General Discoverable Mode and BR/EDR not supported.

static btstack_timer_source_t heartbeat;
static hci_con_handle_t con_handle = HCI_CON_HANDLE_INVALID;
static btstack_context_callback_registration_t send_request;
static btstack_packet_callback_registration_t  hci_event_callback_registration;

const u164">int8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  2, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 
  // Name
  5, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, '6', '-','5', '6', 
  // UUID ...
  17, BLUETOOTH_DATA_TYPE_COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, 0x1, 0xd7, 0xe9, 0x1, 0x4f, 0xf3, 0x44, 0xe7, 0x83, 0x8f, 0xe2, 0x26, 0xb9, 0xe1, 0x56, 0x24,
};

const u164">int8_t adv_data_len = sizeof(adv_data);

Heartbeat Handler

The heartbeat handler updates the value of the single Characteristic provided in this example, and request a ATT_EVENT_CAN_SEND_NOW to send a notification if enabled see Listing here.

static int  counter = 0;
static char counter_string[30];
static 164">int  counter_string_len;

static void beat(void){
  counter++;
  counter_string_len = snpr164">intf(counter_string, sizeof(counter_string), "BTstack counter %03u", counter);
}

static void ublox_can_send(void * context){
  UNUSED(context);
  beat();
  pr164">intf("SEND: %s\n", counter_string);
  ublox_spp_service_server_send(con_handle, (u164">int8_t*) counter_string, counter_string_len);
}

static void heartbeat_handler(struct btstack_timer_source *ts){
  if (con_handle != HCI_CON_HANDLE_INVALID) {
    send_request.callback = &ublox_can_send;
    ublox_spp_service_server_request_can_send_now(&send_request, con_handle);
  }
  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

Packet Handler

The packet handler is used to:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        case HCI_EVENT_DISCONNECTION_COMPLETE:
          con_handle = HCI_CON_HANDLE_INVALID;
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

LE Central - Test Pairing Methods

Source Code: sm_pairing_central.c

Depending on the Authentication requiremens and IO Capabilities, the pairing process uses different short and long term key generation method. This example helps explore the different options incl. LE Secure Connections. It scans for advertisements and connects to the first device that lists a random service.

GAP LE setup for receiving advertisements

GAP LE advertisements are received as custom HCI events of the GAP_EVENT_ADVERTISING_REPORT type. To receive them, you'll need to register the HCI packet handler, as shown in Listing here.

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void sm_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void sm_pairing_central_setup(void){
  l2cap_init();

  // setup SM: Display only
  sm_init();

  // setup ATT server
  att_server_init(profile_data, NULL, NULL);

  // setup GATT Client
  gatt_client_init();

  // register handler
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  sm_event_callback_registration.callback = &sm_packet_handler;
  sm_add_event_handler(&sm_event_callback_registration);


  // Configuration

  // Enable mandatory authentication for GATT Client
  // - if un-encrypted connections are not supported, e.g. when connecting to own device, this enforces authentication
  // gatt_client_set_required_security_165">level(LEVEL_2);

  /**
   * Choose ONE of the following configurations
   * Bonding is disabled to allow for repeated testing. It can be enabled by or'ing
   * SM_AUTHREQ_BONDING to the authentication requirements like this:
   * sm_set_authentication_requirements( X | SM_AUTHREQ_BONDING)

  // LE Legacy Pairing, Just Works
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_YES_NO);
  // sm_set_authentication_requirements(0);

  // LE Legacy Pairing, Passkey entry initiator enter, responder (us) displays
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION);
  // sm_use_fixed_passkey_in_display_role(FIXED_PASSKEY);

#ifdef ENABLE_LE_SECURE_CONNECTIONS

  // enable LE Secure Connections Only mode - disables Legacy pairing
  // sm_set_secure_connections_only_mode(true);

  // LE Secure Connections, Just Works
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_YES_NO);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION);

  // LE Secure Connections, Numeric Comparison
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_YES_NO);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);

  // LE Secure Pairing, Passkey entry initiator (us) enters, responder displays
  // sm_set_io_capabilities(IO_CAPABILITY_KEYBOARD_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);
  // sm_use_fixed_passkey_in_display_role(FIXED_PASSKEY);

  // LE Secure Pairing, Passkey entry initiator (us) displays, responder enters
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);
#endif
}

HCI packet handler

The HCI packet handler has to start the scanning, and to handle received advertisements. Advertisements are received as HCI event packets of the GAP_EVENT_ADVERTISING_REPORT type, see Listing here.

HCI packet handler

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;
  hci_con_handle_t con_handle;
  u164">int8_t status;

  switch (hci_event_packet_get_type(packet)) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
        pr164">intf("Start scaning!\n");
        gap_set_scan_parameters(1,0x0030, 0x0030);
        gap_start_scan(); 
      }
      break;
    case GAP_EVENT_ADVERTISING_REPORT:{
      bd_addr_t address;
      gap_event_advertising_report_get_address(packet, address);
      u164">int8_t address_type = gap_event_advertising_report_get_address_type(packet);
      u164">int8_t length = gap_event_advertising_report_get_data_length(packet);
      const u164">int8_t * data = gap_event_advertising_report_get_data(packet);
      // pr164">intf("Advertisement event: addr-type %u, addr %s, data[%u] ",
      //   address_type, bd_addr_to_str(address), length);
      // pr164">intf_hexdump(data, length);
      if (!ad_data_contains_uuid16(length, (u164">int8_t *) data, REMOTE_SERVICE)) break;
      pr164">intf("Found remote with UUID %04x, connecting...\n", REMOTE_SERVICE);
      gap_stop_scan();
      gap_connect(address,address_type);
      break;
    }
    case HCI_EVENT_META_GAP:
      // wait for connection complete
      if (hci_event_gap_meta_get_subevent_code(packet) != GAP_SUBEVENT_LE_CONNECTION_COMPLETE) break;
      con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
      pr164">intf("Connection complete\n");

      // for testing, choose one of the following actions

      // manually start pairing
      sm_request_pairing(con_handle);

      // gatt client request to authenticated characteristic in sm_pairing_peripheral (short cut, uses hard-coded value handle)
      // gatt_client_read_value_of_characteristic_using_value_handle(&hci_packet_handler, con_handle, 0x0009);

      // general gatt client request to trigger mandatory authentication
      // gatt_client_discover_primary_services(&hci_packet_handler, con_handle);
      break;
    case GATT_EVENT_QUERY_COMPLETE:
      status = gatt_event_query_complete_get_att_status(packet);
      switch (status){
        case ATT_ERROR_INSUFFICIENT_ENCRYPTION:
          pr164">intf("GATT Query result: Insufficient Encryption\n");
          break;
        case ATT_ERROR_INSUFFICIENT_AUTHENTICATION:
          pr164">intf("GATT Query result: Insufficient Authentication\n");
          break;
        case ATT_ERROR_BONDING_INFORMATION_MISSING:
          pr164">intf("GATT Query result: Bonding Information Missing\n");
          break;
        case ATT_ERROR_SUCCESS:
          pr164">intf("GATT Query result: OK\n");
          break;
        default:
          pr164">intf("GATT Query result: 0x%02x\n", gatt_event_query_complete_get_att_status(packet));
          break;
      }
      break;
    default:
      break;
  }
}

 *


static void sm_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  bd_addr_t addr;
  bd_addr_type_t addr_type;

  switch (hci_event_packet_get_type(packet)) {
    case SM_EVENT_JUST_WORKS_REQUEST:
      pr164">intf("Just works requested\n");
      sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
      break;
    case SM_EVENT_NUMERIC_COMPARISON_REQUEST:
      pr164">intf("Confirming numeric comparison: %"PRIu32"\n", sm_event_numeric_comparison_request_get_passkey(packet));
      sm_numeric_comparison_confirm(sm_event_passkey_display_number_get_handle(packet));
      break;
    case SM_EVENT_PASSKEY_DISPLAY_NUMBER:
      pr164">intf("Display Passkey: %"PRIu32"\n", sm_event_passkey_display_number_get_passkey(packet));
      break;
    case SM_EVENT_PASSKEY_INPUT_NUMBER:
      pr164">intf("Passkey Input requested\n");
      pr164">intf("Sending fixed passkey %"PRIu32"\n", (u164">int32_t) FIXED_PASSKEY);
      sm_passkey_input(sm_event_passkey_input_number_get_handle(packet), FIXED_PASSKEY);
      break;
    case SM_EVENT_PAIRING_STARTED:
      pr164">intf("Pairing started\n");
      break;
    case SM_EVENT_PAIRING_COMPLETE:
      switch (sm_event_pairing_complete_get_status(packet)){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Pairing complete, success\n");
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          pr164">intf("Pairing failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          pr164">intf("Pairing failed, disconnected\n");
          break;
        case ERROR_CODE_AUTHENTICATION_FAILURE:
          pr164">intf("Pairing failed, authentication failure with reason = %u\n", sm_event_pairing_complete_get_reason(packet));
          break;
        default:
          break;
      }
      break;
    case SM_EVENT_REENCRYPTION_STARTED:
      sm_event_reencryption_complete_get_address(packet, addr);
      pr164">intf("Bonding information exists for addr type %u, identity addr %s -> start re-encryption\n",
           sm_event_reencryption_started_get_addr_type(packet), bd_addr_to_str(addr));
      break;
    case SM_EVENT_REENCRYPTION_COMPLETE:
      switch (sm_event_reencryption_complete_get_status(packet)){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Re-encryption complete, success\n");
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          pr164">intf("Re-encryption failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          pr164">intf("Re-encryption failed, disconnected\n");
          break;
        case ERROR_CODE_PIN_OR_KEY_MISSING:
          pr164">intf("Re-encryption failed, bonding information missing\n\n");
          pr164">intf("Assuming remote lost bonding information\n");
          pr164">intf("Deleting local bonding information and start new pairing...\n");
          sm_event_reencryption_complete_get_address(packet, addr);
          addr_type = sm_event_reencryption_started_get_addr_type(packet);
          gap_delete_bonding(addr_type, addr);
          sm_request_pairing(sm_event_reencryption_complete_get_handle(packet));
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

The SM packet handler receives Security Manager Events required for pairing. It also receives events generated during Identity Resolving see Listing here.

LE Peripheral - Test Pairing Methods

Source Code: sm_pairing_peripheral.c

Depending on the Authentication requiremens and IO Capabilities, the pairing process uses different short and long term key generation method. This example helps explore the different options incl. LE Secure Connections.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager and configures the ATT Server with the pre-compiled ATT Database generated from $sm_pairing_peripheral.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack. In this example, the Advertisement contains the Flags attribute, the device name, and a 16-bit (test) service 0x1111 The flag 0x06 indicates: LE General Discoverable Mode and BR/EDR not supported. Various examples for IO Capabilites and Authentication Requirements are given below.

static btstack_packet_callback_registration_t sm_event_callback_registration;

static void packet_handler (u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

const u164">int8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 
  // Name
  0x0b, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'S', 'M', ' ', 'P', 'a', 'i', 'r', 'i', 'n', 'g', 
  // Incomplete List of 16-bit Service Class UUIDs -- 1111 - only valid for testing!
  0x03, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0x11, 0x11,
};
const u164">int8_t adv_data_len = sizeof(adv_data);

static void sm_peripheral_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

  // setup ATT server
  att_server_init(profile_data, NULL, NULL);

  // setup GATT Client
  gatt_client_init();

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // register for SM events
  sm_event_callback_registration.callback = &packet_handler;
  sm_add_event_handler(&sm_event_callback_registration);

  // register for ATT
  1">att_server_register_packet_handler(packet_handler);


  // Configuration

  // Enable mandatory authentication for GATT Client
  // - if un-encrypted connections are not supported, e.g. when connecting to own device, this enforces authentication
  // gatt_client_set_required_security_165">level(LEVEL_2);

  /**
   * Choose ONE of the following configurations
   * Bonding is disabled to allow for repeated testing. It can be enabled by or'ing
   * SM_AUTHREQ_BONDING to the authentication requirements like this:
   * sm_set_authentication_requirements( X | SM_AUTHREQ_BONDING)

  // LE Legacy Pairing, Just Works
  // sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
  // sm_set_authentication_requirements(0);

  // LE Legacy Pairing, Passkey entry initiator enter, responder (us) displays
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_MITM_PROTECTION);
  // sm_use_fixed_passkey_in_display_role(123456);

#ifdef ENABLE_LE_SECURE_CONNECTIONS

  // enable LE Secure Connections Only mode - disables Legacy pairing
  // sm_set_secure_connections_only_mode(true);

  // LE Secure Connections, Just Works
  // sm_set_io_capabilities(IO_CAPABILITY_NO_INPUT_NO_OUTPUT);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION);

  // LE Secure Connections, Numeric Comparison
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_YES_NO);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);

  // LE Secure Pairing, Passkey entry initiator enter, responder (us) displays
  // sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);
  // sm_use_fixed_passkey_in_display_role(123456);

  // LE Secure Pairing, Passkey entry initiator displays, responder (us) enter
  // sm_set_io_capabilities(IO_CAPABILITY_KEYBOARD_ONLY);
  // sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);
#endif
}

Packet Handler

The packet handler is used to:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  hci_con_handle_t con_handle;
  bd_addr_t addr;
  bd_addr_type_t addr_type;
  u164">int8_t status;

  switch (hci_event_packet_get_type(packet)) {
    case HCI_EVENT_META_GAP:
      switch (hci_event_gap_meta_get_subevent_code(packet)) {
        case GAP_SUBEVENT_LE_CONNECTION_COMPLETE:
          pr164">intf("Connection complete\n");
          con_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
          UNUSED(con_handle);

          // for testing, choose one of the following actions

          // manually start pairing
          // sm_request_pairing(con_handle);

          // gatt client request to authenticated characteristic in sm_pairing_central (short cut, uses hard-coded value handle)
          // gatt_client_read_value_of_characteristic_using_value_handle(&packet_handler, con_handle, 0x0009);

          // general gatt client request to trigger mandatory authentication
          // gatt_client_discover_primary_services(&packet_handler, con_handle);
          break;
        default:
          break;
      }
      break;
    case SM_EVENT_JUST_WORKS_REQUEST:
      pr164">intf("Just Works requested\n");
      sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
      break;
    case SM_EVENT_NUMERIC_COMPARISON_REQUEST:
      pr164">intf("Confirming numeric comparison: %"PRIu32"\n", sm_event_numeric_comparison_request_get_passkey(packet));
      sm_numeric_comparison_confirm(sm_event_passkey_display_number_get_handle(packet));
      break;
    case SM_EVENT_PASSKEY_DISPLAY_NUMBER:
      pr164">intf("Display Passkey: %"PRIu32"\n", sm_event_passkey_display_number_get_passkey(packet));
      break;
    case SM_EVENT_IDENTITY_CREATED:
      sm_event_identity_created_get_identity_address(packet, addr);
      pr164">intf("Identity created: type %u address %s\n", sm_event_identity_created_get_identity_addr_type(packet), bd_addr_to_str(addr));
      break;
    case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED:
      sm_event_identity_resolving_succeeded_get_identity_address(packet, addr);
      pr164">intf("Identity resolved: type %u address %s\n", sm_event_identity_resolving_succeeded_get_identity_addr_type(packet), bd_addr_to_str(addr));
      break;
    case SM_EVENT_IDENTITY_RESOLVING_FAILED:
      sm_event_identity_created_get_address(packet, addr);
      pr164">intf("Identity resolving failed\n");
      break;
    case SM_EVENT_PAIRING_STARTED:
      pr164">intf("Pairing started\n");
      break;
    case SM_EVENT_PAIRING_COMPLETE:
      switch (sm_event_pairing_complete_get_status(packet)){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Pairing complete, success\n");
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          pr164">intf("Pairing failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          pr164">intf("Pairing failed, disconnected\n");
          break;
        case ERROR_CODE_AUTHENTICATION_FAILURE:
          pr164">intf("Pairing failed, authentication failure with reason = %u\n", sm_event_pairing_complete_get_reason(packet));
          break;
        default:
          break;
      }
      break;
    case SM_EVENT_REENCRYPTION_STARTED:
      sm_event_reencryption_complete_get_address(packet, addr);
      pr164">intf("Bonding information exists for addr type %u, identity addr %s -> re-encryption started\n",
           sm_event_reencryption_started_get_addr_type(packet), bd_addr_to_str(addr));
      break;
    case SM_EVENT_REENCRYPTION_COMPLETE:
      switch (sm_event_reencryption_complete_get_status(packet)){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Re-encryption complete, success\n");
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          pr164">intf("Re-encryption failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          pr164">intf("Re-encryption failed, disconnected\n");
          break;
        case ERROR_CODE_PIN_OR_KEY_MISSING:
          pr164">intf("Re-encryption failed, bonding information missing\n\n");
          pr164">intf("Assuming remote lost bonding information\n");
          pr164">intf("Deleting local bonding information to allow for new pairing...\n");
          sm_event_reencryption_complete_get_address(packet, addr);
          addr_type = sm_event_reencryption_started_get_addr_type(packet);
          gap_delete_bonding(addr_type, addr);
          break;
        default:
          break;
      }
      break;
    case GATT_EVENT_QUERY_COMPLETE:
      status = gatt_event_query_complete_get_att_status(packet);
      switch (status){
        case ATT_ERROR_INSUFFICIENT_ENCRYPTION:
          pr164">intf("GATT Query failed, Insufficient Encryption\n");
          break;
        case ATT_ERROR_INSUFFICIENT_AUTHENTICATION:
          pr164">intf("GATT Query failed, Insufficient Authentication\n");
          break;
        case ATT_ERROR_BONDING_INFORMATION_MISSING:
          pr164">intf("GATT Query failed, Bonding Information Missing\n");
          break;
        case ATT_ERROR_SUCCESS:
          pr164">intf("GATT Query successful\n");
          break;
        default:
          pr164">intf("GATT Query failed, status 0x%02x\n", gatt_event_query_complete_get_att_status(packet));
          break;
      }
      break;
    default:
      break;
  }
}

LE Credit-Based Flow-Control Mode Client - Send Data over L2CAP

Source Code: le_credit_based_flow_control_mode_client.c

Connects to 'LE CBM Server' and streams data via L2CAP Channel in LE Credit-Based Flow-Control Mode (CBM)

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

#define REPORT_INTERVAL_MS 3000

// support for multiple clients
typedef struct {
  char name;
  hci_con_handle_t connection_handle;
  144">u164">int16_t cid;
  164">int  counter;
  char test_data[TEST_PACKET_SIZE];
  164">int  test_data_len;
  u164">int32_t test_data_sent;
  u164">int32_t test_data_start;
} le_cbm_connection_t;

static le_cbm_connection_t le_cbm_connection;

static void test_reset(le_cbm_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_data(le_cbm_connection_t * context, 164">int bytes_transferred){
  context->test_data_sent += bytes_transferred;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes -> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  // create test data
  le_cbm_connection.counter++;
  if (le_cbm_connection.counter > 'Z') le_cbm_connection.counter = 'A';
  memset(le_cbm_connection.test_data, le_cbm_connection.counter, le_cbm_connection.test_data_len);

  // send
  l2cap_send(le_cbm_connection.cid, (u164">int8_t *) le_cbm_connection.test_data, le_cbm_connection.test_data_len);

  // track
  test_track_data(&le_cbm_connection, le_cbm_connection.test_data_len);

  // request another packet
  l2cap_request_can_send_now_event(le_cbm_connection.cid);
}

SM Packet Handler

The packet handler is used to handle pairing requests

LE Credit-Based Flow-Control Mode Server - Receive data over L2CAP

Source Code: le_credit_based_flow_control_mode_server.c

iOS 11 and newer supports L2CAP channels in LE Credit-Based Flow-Control Mode for fast transfer over LE https://github.com/bluekitchen/CBL2CAPChannel-Demo

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(le_cbm_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_data(le_cbm_connection_t * context, 164">int bytes_transferred){
  context->test_data_sent += bytes_transferred;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  if (le_cbm_connection.cid == 0) return;

  // create test data
  le_cbm_connection.counter++;
  if (le_cbm_connection.counter > 'Z') le_cbm_connection.counter = 'A';
  memset(le_cbm_connection.test_data, le_cbm_connection.counter, le_cbm_connection.test_data_len);

  // send
  l2cap_send(le_cbm_connection.cid, (u164">int8_t *) le_cbm_connection.test_data, le_cbm_connection.test_data_len);

  // track
  test_track_data(&le_cbm_connection, le_cbm_connection.test_data_len);

  // request another packet
  l2cap_request_can_send_now_event(le_cbm_connection.cid);
}

HCI + L2CAP Packet Handler

The packet handler is used to stop the notifications and reset the MTU on connect It would also be a good place to request the connection parameter update as indicated in the commented code block.

SM Packet Handler

Main Application Setup

LE Peripheral - Delayed Response

Source Code: att_delayed_response.c

The packet handler is used to handle pairing requests

Listing here shows main application code. It initializes L2CAP, the Security Manager, and configures the ATT Server with the pre-compiled ATT Database generated from $le_credit_based_flow_control_mode_server.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack.

If the value for a GATT Chararacteristic isn't availabl for read, the value ATT_READ_RESPONSE_PENDING can be returned. When the value is available, att_server_response_ready is then called to complete the ATT request.

Similarly, the error code ATT_ERROR_WRITE_RESPONSE_PENING can be returned when it is unclear if a write can be performed or not. When the decision was made, att_server_response_ready is is then called to complete the ATT request.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager and configures the ATT Server with the pre-compiled ATT Database generated from $att_delayed_response.gatt$. Additionally, it enables the Battery Service Server with the current battery level. Finally, it configures the advertisements and boots the Bluetooth stack. In this example, the Advertisement contains the Flags attribute and the device name. The flag 0x06 indicates: LE General Discoverable Mode and BR/EDR not supported.

#ifdef ENABLE_ATT_DELAYED_RESPONSE
static btstack_timer_source_t att_timer;
static hci_con_handle_t con_handle;
static 164">int value_ready;
#endif

static 144">u164">int16_t att_read_callback(hci_con_handle_t con_handle, 144">u164">int16_t att_handle, 144">u164">int16_t offset, u164">int8_t * buffer, 144">u164">int16_t buffer_size);
static 164">int att_write_callback(hci_con_handle_t connection_handle, 144">u164">int16_t att_handle, 144">u164">int16_t transaction_mode, 144">u164">int16_t offset, u164">int8_t *buffer, 144">u164">int16_t buffer_size);

const u164">int8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  0x02, 0x01, 0x06, 
  // Name
  0x08, 0x09, 'D', 'e', 'l', 'a', 'y', 'e', 'd', 
};
const u164">int8_t adv_data_len = sizeof(adv_data);

const char * test_string = "Delayed response";

static void example_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

  // setup ATT server
  att_server_init(profile_data, att_read_callback, att_write_callback);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);
}

att_invalidate_value Handler

The att_invalidate_value handler 'invalidates' the value of the single Characteristic provided in this example

att_update_value Handler

The att_update_value handler 'updates' the value of the single Characteristic provided in this example

#ifdef ENABLE_ATT_DELAYED_RESPONSE
static void att_update_value(struct btstack_timer_source *ts){
  UNUSED(ts);
  value_ready = 1;

  // trigger ATT Server to try request again
  164">int status = 163">att_server_response_ready(con_handle);

  pr164">intf("Value updated -> complete ATT request - status 0x%02x\n", status);

  // simulated value becoming stale again
  att_timer.process = &att_invalidate_value;
  btstack_run_loop_set_timer(&att_timer, ATT_VALUE_DELAY_MS);
  btstack_run_loop_add_timer(&att_timer);
} 
#endif

ATT Read

The ATT Server handles all reads to constant data. For dynamic data like the custom characteristic, the registered att_read_callback is called. To handle long characteristics and long reads, the att_read_callback is first called with buffer == NULL, to request the total value length. Then it will be called again requesting a chunk of the value. See Listing here.

ATT Write

// ATT Client Read Callback for Dynamic Data
// - if buffer == NULL, don't copy data, just return size of value
// - if buffer != NULL, copy data and return number bytes copied
// @param offset defines start of attribute value
static 144">u164">int16_t att_read_callback(hci_con_handle_t connection_handle, 144">u164">int16_t att_handle, 144">u164">int16_t offset, u164">int8_t * buffer, 144">u164">int16_t buffer_size){

#ifdef ENABLE_ATT_DELAYED_RESPONSE
  switch (att_handle){
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
      if (value_ready){
        return att_read_callback_handle_blob((const u164">int8_t *)test_string, strlen(test_string), offset, buffer, buffer_size);
      } else {
        pr164">intf("Read callback for handle 0x%02x, but value not ready -> report response pending\n", att_handle);
        con_handle = connection_handle;
        return ATT_READ_RESPONSE_PENDING;
      }
      break;
    case ATT_READ_RESPONSE_PENDING:
      // virtual handle indicating all attributes have been queried
      pr164">intf("Read callback for virtual handle 0x%02x - all attributes have been queried (important for read multiple or read by type) -> start updating values\n", att_handle);
      // simulated delayed response for example
      att_timer.process = &att_update_value;
      btstack_run_loop_set_timer(&att_timer, ATT_VALUE_DELAY_MS);
      btstack_run_loop_add_timer(&att_timer);
      return 0;
    default:
      break;
  }
#else
  UNUSED(connection_handle);
  // useless code when ENABLE_ATT_DELAYED_RESPONSE is not defined - but avoids built errors
  if (att_handle == ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE){
    pr164">intf("ENABLE_ATT_DELAYED_RESPONSE not defined in btstack_config.h, responding right away");
    return att_read_callback_handle_blob((const u164">int8_t *)test_string, (144">u164">int16_t) strlen(test_string), offset, buffer, buffer_size);
  }
#endif

  return 0;
}

/*

static 164">int att_write_callback(hci_con_handle_t connection_handle, 144">u164">int16_t att_handle, 144">u164">int16_t transaction_mode, 144">u164">int16_t offset, u164">int8_t *buffer, 144">u164">int16_t buffer_size){
  UNUSED(transaction_mode);
  UNUSED(offset);
  UNUSED(buffer_size);
  UNUSED(connection_handle);

  if (att_handle == ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE) {
    pr164">intf("Write request, value: ");
    pr164">intf_hexdump(buffer, buffer_size);
#ifdef ENABLE_ATT_DELAYED_RESPONSE
    if (value_ready){
      pr164">intf("Write callback, value ready\n");
      return 0;          
    } else {
      pr164">intf("Write callback for handle 0x%02x, but not ready -> return response pending\n", att_handle);
    }
    // simulated delayed response for example
    att_timer.process = &att_update_value;
    btstack_run_loop_set_timer(&att_timer, ATT_VALUE_DELAY_MS);
    btstack_run_loop_add_timer(&att_timer);
    return ATT_ERROR_WRITE_RESPONSE_PENDING;
#else
    pr164">intf("ENABLE_ATT_DELAYED_RESPONSE not defined in btstack_config.h, responding right away");
    return 0;
#endif
  }
  return 0;
}

LE ANCS Client - Apple Notification Service

Source Code: ancs_client_demo.c

LE Man-in-the-Middle Tool

Source Code: le_mitm.c

The example first does an LE scan and allows the user to select a Peripheral device. Then, it connects to the Peripheral and starts advertising with the same data as the Peripheral device. ATT Requests and responses are forwarded between the peripheral and the central Security requests are handled locally.

@note A Bluetooth Controller that supports Central and Peripheral Role at the same time is required for this example. See chipset/README.md

Performance - Stream Data over GATT (Client)

Source Code: le_streamer_client.c

Connects to 'LE Streamer' and subscribes to test characteristic

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

#define TEST_MODE_WRITE_WITHOUT_RESPONSE 1
#define TEST_MODE_ENABLE_NOTIFICATIONS   2
#define TEST_MODE_DUPLEX         3

// configure test mode: send only, receive only, full duplex
#define TEST_MODE TEST_MODE_DUPLEX

#define REPORT_INTERVAL_MS 3000

// support for multiple clients
static le_streamer_connection_t le_streamer_connection;

static void test_reset(le_streamer_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_data(le_streamer_connection_t * context, 164">int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes -> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

Performance - Stream Data over GATT (Server)

Source Code: gatt_streamer_server.c

All newer operating systems provide GATT Client functionality. This example shows how to get a maximal throughput via BLE:

In theory, we should also update the connection parameters, but we already get a connection interval of 30 ms and there's no public way to use a shorter interval with iOS (if we're not implementing an HID device).

Note: To start the streaming, run the example. On remote device use some GATT Explorer, e.g. LightBlue, BLExplr to enable notifications.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager, and configures the ATT Server with the pre-compiled ATT Database generated from $le_streamer.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack.

static void le_streamer_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

#ifdef ENABLE_GATT_OVER_CLASSIC
  // init SDP, create record for GATT and register with SDP
  sdp_init();
  memset(gatt_service_buffer, 0, sizeof(gatt_service_buffer));
  gatt_create_sdp_record(gatt_service_buffer, 0x10001, ATT_SERVICE_GATT_SERVICE_START_HANDLE, ATT_SERVICE_GATT_SERVICE_END_HANDLE);
  sdp_register_service(gatt_service_buffer);
  pr164">intf("SDP service record size: %u\n", de_get_len(gatt_service_buffer));

  // configure Classic GAP
  gap_set_local_name("GATT Streamer BR/EDR 00:00:00:00:00:00");
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_discoverable_control(1);
#endif

  // setup ATT server
  att_server_init(profile_data, NULL, att_write_callback);

  // register for HCI events
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for ATT events
  1">att_server_register_packet_handler(att_packet_handler);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // init client state
  init_connections();
}

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(le_streamer_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_sent(le_streamer_connection_t * context, 164">int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

HCI Packet Handler

The packet handler is used track incoming connections and to stop notifications on disconnect It is also a good place to request the connection parameter update as indicated in the commented code block.

static void hci_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  144">u164">int16_t conn_164">interval;
  hci_con_handle_t con_handle;
  static const char * const phy_names[] = {
    "1 M", "2 M", "Codec"
  };

  switch (hci_event_packet_get_type(packet)) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
        pr164">intf("To start the streaming, please run the le_streamer_client example on other device, or use some GATT Explorer, e.g. LightBlue, BLExplr.\n");
      } 
      break;
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      con_handle = hci_event_disconnection_complete_get_connection_handle(packet);
      pr164">intf("- LE Connection 0x%04x: disconnect, reason %02x\n", con_handle, hci_event_disconnection_complete_get_reason(packet));          
      break;
    case HCI_EVENT_META_GAP:
      switch (hci_event_gap_meta_get_subevent_code(packet)) {
        case GAP_SUBEVENT_LE_CONNECTION_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = gap_subevent_le_connection_complete_get_connection_handle(packet);
          conn_164">interval = gap_subevent_le_connection_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connected - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), gap_subevent_le_connection_complete_get_conn_latency(packet));

          // request min con 164">interval 15 ms for iOS 11+
          pr164">intf("- LE Connection 0x%04x: request 15 ms connection 164">interval\n", con_handle);
          gap_request_connection_parameter_update(con_handle, 12, 12, 4, 0x0048);
          break;
        default:
          break;
      }
      break;
    case HCI_EVENT_LE_META:
      switch (hci_event_le_meta_get_subevent_code(packet)) {
        case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = hci_subevent_le_connection_update_complete_get_connection_handle(packet);
          conn_164">interval = hci_subevent_le_connection_update_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connection update - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));
          break;
        case HCI_SUBEVENT_LE_DATA_LENGTH_CHANGE:
          con_handle = hci_subevent_le_data_length_change_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: data length change - max %u bytes per packet\n", con_handle,
               hci_subevent_le_data_length_change_get_max_tx_octets(packet));
          break;
        case HCI_SUBEVENT_LE_PHY_UPDATE_COMPLETE:
          con_handle = hci_subevent_le_phy_update_complete_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: PHY update - using LE %s PHY now\n", con_handle,
               phy_names[hci_subevent_le_phy_update_complete_get_tx_phy(packet)]);
          break;
        default:
          break;
      }
      break;

    default:
      break;
  }
}

ATT Packet Handler

The packet handler is used to track the ATT MTU Exchange and trigger ATT send

static void att_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  164">int mtu;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        case ATT_EVENT_CONNECTED:
          // setup new 
          context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
          if (!context) break;
          context->counter = 'A';
          context->connection_handle = att_event_connected_get_handle(packet);
          context->test_data_len = btstack_min(103">att_server_get_mtu(context->connection_handle) - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT connected, handle 0x%04x, test data len %u\n", context->name, context->connection_handle, context->test_data_len);
          break;
        case ATT_EVENT_MTU_EXCHANGE_COMPLETE:
          mtu = att_event_mtu_exchange_complete_get_MTU(packet) - 3;
          context = connection_for_conn_handle(att_event_mtu_exchange_complete_get_handle(packet));
          if (!context) break;
          context->test_data_len = btstack_min(mtu - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT MTU = %u => use test data of len %u\n", context->name, mtu, context->test_data_len);
          break;
        case ATT_EVENT_CAN_SEND_NOW:
          streamer();
          break;
        case ATT_EVENT_DISCONNECTED:
          context = connection_for_conn_handle(att_event_disconnected_get_handle(packet));
          if (!context) break;
          // free connection
          pr164">intf("%c: ATT disconnected, handle 0x%04x\n", context->name, context->connection_handle);          
          context->le_notification_enabled = 0;
          context->connection_handle = HCI_CON_HANDLE_INVALID;
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  // find next active streaming connection
  164">int old_connection_index = connection_index;
  while (1){
    // active found?
    if ((le_streamer_connections[connection_index].connection_handle != HCI_CON_HANDLE_INVALID) &&
      (le_streamer_connections[connection_index].le_notification_enabled)) break;

    // check next
    next_connection_index();

    // none found
    if (connection_index == old_connection_index) return;
  }

  le_streamer_connection_t * context = &le_streamer_connections[connection_index];

  // create test data
  context->counter++;
  if (context->counter > 'Z') context->counter = 'A';
  memset(context->test_data, context->counter, context->test_data_len);

  // send
  131">att_server_notify(context->connection_handle, context->value_handle, (u164">int8_t*) context->test_data, context->test_data_len);

  // track
  test_track_sent(context, context->test_data_len);

  // request next send event
  186">att_server_request_can_send_now_event(context->connection_handle);

  // check next
  next_connection_index();
}

ATT Write

The only valid ATT write in this example is to the Client Characteristic Configuration, which configures notification and indication. If the ATT handle matches the client configuration handle, the new configuration value is stored. If notifications get enabled, an ATT_EVENT_CAN_SEND_NOW is requested. See Listing here.

static int att_write_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
  UNUSED(offset);

  // pr164">intf("att_write_callback att_handle 0x%04x, transaction mode %u\n", att_handle, transaction_mode);
  if (transaction_mode != ATT_TRANSACTION_MODE_NONE) return 0;
  le_streamer_connection_t * context = connection_for_conn_handle(con_handle);
  switch(att_handle){
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
      context->le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
      pr164">intf("%c: Notifications enabled %u\n", context->name, context->le_notification_enabled); 
      if (context->le_notification_enabled){
        switch (att_handle){
          case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          default:
            break;
        }
        186">att_server_request_can_send_now_event(context->connection_handle);
      }
      test_reset(context);
      break;
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
      test_track_sent(context, buffer_size);
      break;
    default:
      pr164">intf("Write to 0x%04x, len %u\n", att_handle, buffer_size);
      break;
  }
  return 0;
}

LE Credit-Based Flow-Control Mode Client - Send Data over L2CAP

Source Code: le_credit_based_flow_control_mode_client.c

Connects to 'LE CBM Server' and streams data via L2CAP Channel in LE Credit-Based Flow-Control Mode (CBM)

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

#define REPORT_INTERVAL_MS 3000

// support for multiple clients
typedef struct {
  char name;
  hci_con_handle_t connection_handle;
  144">u164">int16_t cid;
  164">int  counter;
  char test_data[TEST_PACKET_SIZE];
  164">int  test_data_len;
  u164">int32_t test_data_sent;
  u164">int32_t test_data_start;
} le_cbm_connection_t;

static le_cbm_connection_t le_cbm_connection;

static void test_reset(le_cbm_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_data(le_cbm_connection_t * context, 164">int bytes_transferred){
  context->test_data_sent += bytes_transferred;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes -> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  // create test data
  le_cbm_connection.counter++;
  if (le_cbm_connection.counter > 'Z') le_cbm_connection.counter = 'A';
  memset(le_cbm_connection.test_data, le_cbm_connection.counter, le_cbm_connection.test_data_len);

  // send
  l2cap_send(le_cbm_connection.cid, (u164">int8_t *) le_cbm_connection.test_data, le_cbm_connection.test_data_len);

  // track
  test_track_data(&le_cbm_connection, le_cbm_connection.test_data_len);

  // request another packet
  l2cap_request_can_send_now_event(le_cbm_connection.cid);
}

SM Packet Handler

The packet handler is used to handle pairing requests

LE Credit-Based Flow-Control Mode Server - Receive data over L2CAP

Source Code: le_credit_based_flow_control_mode_server.c

iOS 11 and newer supports L2CAP channels in LE Credit-Based Flow-Control Mode for fast transfer over LE https://github.com/bluekitchen/CBL2CAPChannel-Demo

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(le_cbm_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_data(le_cbm_connection_t * context, 164">int bytes_transferred){
  context->test_data_sent += bytes_transferred;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  if (le_cbm_connection.cid == 0) return;

  // create test data
  le_cbm_connection.counter++;
  if (le_cbm_connection.counter > 'Z') le_cbm_connection.counter = 'A';
  memset(le_cbm_connection.test_data, le_cbm_connection.counter, le_cbm_connection.test_data_len);

  // send
  l2cap_send(le_cbm_connection.cid, (u164">int8_t *) le_cbm_connection.test_data, le_cbm_connection.test_data_len);

  // track
  test_track_data(&le_cbm_connection, le_cbm_connection.test_data_len);

  // request another packet
  l2cap_request_can_send_now_event(le_cbm_connection.cid);
}

HCI + L2CAP Packet Handler

The packet handler is used to stop the notifications and reset the MTU on connect It would also be a good place to request the connection parameter update as indicated in the commented code block.

SM Packet Handler

Main Application Setup

Performance - Stream Data over SPP (Client)

Source Code: spp_streamer_client.c

The packet handler is used to handle pairing requests

Listing here shows main application code. It initializes L2CAP, the Security Manager, and configures the ATT Server with the pre-compiled ATT Database generated from $le_credit_based_flow_control_mode_server.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack.

Note: The SPP Streamer Client scans for and connects to SPP Streamer, and measures the throughput.

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

#define REPORT_INTERVAL_MS 3000
static u164">int32_t test_data_transferred;
static u164">int32_t test_data_start;

static void test_reset(void){
  test_data_start = btstack_run_loop_get_time_ms();
  test_data_transferred = 0;
}

static void test_track_transferred(164">int bytes_sent){
  test_data_transferred += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = test_data_transferred * 1000 / time_passed;
  pr164">intf("%u bytes -> %u.%03u kB/s\n", (164">int) test_data_transferred, (164">int) bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  test_data_start = now;
  test_data_transferred  = 0;
}

SDP Query Packet Handler

Store RFCOMM Channel for SPP service and initiates RFCOMM connection

Gerenal Packet Handler

Handles startup (BTSTACK_EVENT_STATE), inquiry, pairing, starts SDP query for SPP service, and RFCOMM connection

Main Application Setup

As with the packet and the heartbeat handlers, the combined app setup contains the code from the individual example setups.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  UNUSED(argc);
  (void)argv;

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  rfcomm_init();

#ifdef ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE_FOR_RFCOMM
  // setup ERTM management
  rfcomm_enable_l2cap_ertm(&rfcomm_ertm_request_handler, &rfcomm_ertm_released_handler);
#endif

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // init SDP
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);

  // turn on!
    hci_power_control(HCI_POWER_ON);

  return 0;
}

Performance - Stream Data over SPP (Server)

Source Code: spp_streamer.c

After RFCOMM connections gets open, request a RFCOMM_EVENT_CAN_SEND_NOW via rfcomm_request_can_send_now_event().

When we get the RFCOMM_EVENT_CAN_SEND_NOW, send data and request another one.

Note: To test, run the example, pair from a remote device, and open the Virtual Serial Port.

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

#define REPORT_INTERVAL_MS 3000
static u164">int32_t test_data_transferred;
static u164">int32_t test_data_start;

static void test_reset(void){
  test_data_start = btstack_run_loop_get_time_ms();
  test_data_transferred = 0;
}

static void test_track_transferred(164">int bytes_sent){
  test_data_transferred += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = test_data_transferred * 1000 / time_passed;
  pr164">intf("%u bytes -> %u.%03u kB/s\n", (164">int) test_data_transferred, (164">int) bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  test_data_start = now;
  test_data_transferred  = 0;
}

Packet Handler

The packet handler of the combined example is just the combination of the individual packet handlers.

Main Application Setup

As with the packet and the heartbeat handlers, the combined app setup contains the code from the individual example setups.

int btstack_main(int argc, const char * argv[])
{
  (void)argc;
  (void)argv;

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  rfcomm_init();
  rfcomm_register_service(packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff);

#ifdef ENABLE_L2CAP_ENHANCED_RETRANSMISSION_MODE_FOR_RFCOMM
  // setup ERTM management
  rfcomm_enable_l2cap_ertm(&rfcomm_ertm_request_handler, &rfcomm_ertm_released_handler);
#endif

  // init SDP, create record for SPP and register with SDP
  sdp_init();
  memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
  spp_create_sdp_record(spp_service_buffer, sdp_create_service_record_handle(), RFCOMM_SERVER_CHANNEL, "SPP Streamer");
  btstack_assert(de_get_len( spp_service_buffer) <= sizeof(spp_service_buffer));
  sdp_register_service(spp_service_buffer);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // short-cut to find other SPP Streamer
  gap_set_class_of_device(TEST_COD);

  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_set_local_name("SPP Streamer 00:00:00:00:00:00");
  gap_discoverable_control(1);

  spp_create_test_data();

  // turn on!
    hci_power_control(HCI_POWER_ON);

  return 0;
}

A2DP Sink - Receive Audio Stream and Control Playback

Source Code: a2dp_sink_demo.c

This A2DP Sink example demonstrates how to use the A2DP Sink service to receive an audio data stream from a remote A2DP Source device. In addition, the AVRCP Controller is used to get information on currently played media, such are title, artist and album, as well as to control the playback, i.e. to play, stop, repeat, etc. If HAVE_BTSTACK_STDIN is set, press SPACE on the console to show the available AVDTP and AVRCP commands.

To test with a remote device, e.g. a mobile phone, pair from the remote device with the demo, then start playing music on the remote device. Alternatively, set the device_addr_string to the Bluetooth address of your remote device in the code, and call connect from the UI.

For more info on BTstack audio, see our blog post A2DP Sink and Source on STM32 F4 Discovery Board.

Main Application Setup

The Listing here shows how to set up AD2P Sink and AVRCP services. Besides calling init() method for each service, you'll also need to register several packet handlers:

To announce A2DP Sink and AVRCP services, you need to create corresponding SDP records and register them with the SDP service.

Note, currently only the SBC codec is supported. If you want to store the audio data in a file, you'll need to define STORE_TO_WAV_FILE. If STORE_TO_WAV_FILE directive is defined, the SBC decoder needs to get initialized when a2dp_sink_packet_handler receives event A2DP_SUBEVENT_STREAM_STARTED. The initialization of the SBC decoder requires a callback that handles PCM data:

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void a2dp_sink_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t * packet, 144">u164">int16_t event_size);
static void handle_l2cap_media_data_packet(u164">int8_t seid, u164">int8_t *packet, 144">u164">int16_t size);
static void avrcp_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void avrcp_controller_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void avrcp_target_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
#ifdef HAVE_BTSTACK_STDIN
static void stdin_process(char cmd);
#endif

static 164">int setup_demo(void){

  // init protocols
  l2cap_init();
  sdp_init();
#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif
#ifdef ENABLE_AVRCP_COVER_ART
  goep_client_init();
  avrcp_cover_art_client_init();
#endif

  // Init profiles
  a2dp_sink_init();
  avrcp_init();
  avrcp_controller_init();
  avrcp_target_init();


  // Configure A2DP Sink
  a2dp_sink_register_packet_handler(&a2dp_sink_packet_handler);
  a2dp_sink_register_media_handler(&handle_l2cap_media_data_packet);
  a2dp_sink_demo_stream_endpo164">int_t * stream_endpo164">int = &a2dp_sink_demo_stream_endpo164">int;
  avdtp_stream_endpo164">int_t * local_stream_endpo164">int = a2dp_sink_create_stream_endpo164">int(AVDTP_AUDIO,
                                             AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities),
                                             stream_endpo164">int->media_sbc_codec_configuration, sizeof(stream_endpo164">int->media_sbc_codec_configuration));
  btstack_assert(local_stream_endpo164">int != NULL);
  // - Store stream enpo164">int's SEP ID, as it is used by A2DP API to identify the stream endpo164">int
  stream_endpo164">int->a2dp_local_seid = avdtp_local_seid(local_stream_endpo164">int);


  // Configure AVRCP Controller + Target
  avrcp_register_packet_handler(&avrcp_packet_handler);
  avrcp_controller_register_packet_handler(&avrcp_controller_packet_handler);
  avrcp_target_register_packet_handler(&avrcp_target_packet_handler);


  // Configure SDP

  // - Create and register A2DP Sink service record
  memset(sdp_avdtp_sink_service_buffer, 0, sizeof(sdp_avdtp_sink_service_buffer));
  a2dp_sink_create_sdp_record(sdp_avdtp_sink_service_buffer, sdp_create_service_record_handle(),
                AVDTP_SINK_FEATURE_MASK_HEADPHONE, NULL, NULL);
  btstack_assert(de_get_len( sdp_avdtp_sink_service_buffer) <= sizeof(sdp_avdtp_sink_service_buffer));
  sdp_register_service(sdp_avdtp_sink_service_buffer);

  // - Create AVRCP Controller service record and register it with SDP. We send Category 1 commands to the media player, e.g. play/pause
  memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer));
  144">u164">int16_t controller_supported_features = 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_CATEGORY_PLAYER_OR_RECORDER;
#ifdef AVRCP_BROWSING_ENABLED
  controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_BROWSING;
#endif
#ifdef ENABLE_AVRCP_COVER_ART
  controller_supported_features |= 1 << AVRCP_CONTROLLER_SUPPORTED_FEATURE_COVER_ART_GET_LINKED_THUMBNAIL;
#endif
  avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(),
                     controller_supported_features, NULL, NULL);
  btstack_assert(de_get_len( sdp_avrcp_controller_service_buffer) <= sizeof(sdp_avrcp_controller_service_buffer));
  sdp_register_service(sdp_avrcp_controller_service_buffer);

  // - Create and register A2DP Sink service record
  //   -  We receive Category 2 commands from the media player, e.g. volume up/down
  memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
  144">u164">int16_t target_supported_features = 1 << AVRCP_TARGET_SUPPORTED_FEATURE_CATEGORY_MONITOR_OR_AMPLIFIER;
  avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer,
                   sdp_create_service_record_handle(), target_supported_features, NULL, NULL);
  btstack_assert(de_get_len( sdp_avrcp_target_service_buffer) <= sizeof(sdp_avrcp_target_service_buffer));
  sdp_register_service(sdp_avrcp_target_service_buffer);

  // - Create and register Device ID (PnP) service record
  memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer));
  device_id_create_sdp_record(device_id_sdp_service_buffer,
                sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
  btstack_assert(de_get_len( device_id_sdp_service_buffer) <= sizeof(device_id_sdp_service_buffer));
  sdp_register_service(device_id_sdp_service_buffer);


  // Configure GAP - discovery / connection

  // - Set local name with a template Bluetooth address, that will be automatically
  //   replaced with an actual address once it is available, i.e. when BTstack boots
  //   up and starts talking to a Bluetooth module.
  gap_set_local_name("A2DP Sink Demo 00:00:00:00:00:00");

  // - Allow to show up in Bluetooth inquiry
  gap_discoverable_control(1);

  // - Set Class of Device - Service Class: Audio, Major Device Class: Audio, Minor: Loudspeaker
  gap_set_class_of_device(0x200414);

  // - Allow for role switch in general and sniff mode
  gap_set_default_link_policy_settings( LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE );

  // - Allow for role switch on outgoing connections
  //   - This allows A2DP Source, e.g. smartphone, to become master when we re-connect to it.
  gap_set_allow_role_switch(true);


  // Register for HCI events
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // Inform about audio playback / test options
#ifdef HAVE_POSIX_FILE_IO
  if (!btstack_audio_sink_get_instance()){
    pr164">intf("No audio playback.\n");
  } else {
    pr164">intf("Audio playback supported.\n");
  }
#ifdef STORE_TO_WAV_FILE 
   pr164">intf("Audio will be stored to \'%s\' file.\n",  wav_filename);
#endif
#endif
  return 0;
}

Handle Media Data Packet

Here the audio data, are received through the handle_l2cap_media_data_packet callback. Currently, only the SBC media codec is supported. Hence, the media data consists of the media packet header and the SBC packet. The SBC frame will be stored in a ring buffer for later processing (instead of decoding it to PCM right away which would require a much larger buffer). If the audio stream wasn't started already and there are enough SBC frames in the ring buffer, start playback.

A2DP Source - Stream Audio and Control Volume

Source Code: a2dp_source_demo.c

This A2DP Source example demonstrates how to send an audio data stream to a remote A2DP Sink device and how to switch between two audio data sources. In addition, the AVRCP Target is used to answer queries on currently played media, as well as to handle remote playback control, i.e. play, stop, repeat, etc. If HAVE_BTSTACK_STDIN is set, press SPACE on the console to show the available AVDTP and AVRCP commands.

To test with a remote device, e.g. a Bluetooth speaker, set the device_addr_string to the Bluetooth address of your remote device in the code, and use the UI to connect and start playback.

For more info on BTstack audio, see our blog post A2DP Sink and Source on STM32 F4 Discovery Board.

Main Application Setup

The Listing here shows how to setup AD2P Source and AVRCP services. Besides calling init() method for each service, you'll also need to register several packet handlers:

To announce A2DP Source and AVRCP services, you need to create corresponding SDP records and register them with the SDP service.

static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void a2dp_source_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t * event, 144">u164">int16_t event_size);
static void avrcp_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void avrcp_target_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void avrcp_controller_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
#ifdef HAVE_BTSTACK_STDIN
static void stdin_process(char cmd);
#endif

static void a2dp_demo_hexcmod_configure_sample_rate(164">int sample_rate);

static 164">int a2dp_source_and_avrcp_services_init(void){

  // Request role change on reconnecting headset to always use them in slave mode
  hci_set_master_slave_policy(0);
  // enabled EIR
  hci_set_inquiry_mode(INQUIRY_MODE_RSSI_AND_EIR);

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // Initialize  A2DP Source
  a2dp_source_init();
  a2dp_source_register_packet_handler(&a2dp_source_packet_handler);

  // Create stream endpo164">int
  avdtp_stream_endpo164">int_t * local_stream_endpo164">int = a2dp_source_create_stream_endpo164">int(AVDTP_AUDIO, AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities), media_sbc_codec_configuration, sizeof(media_sbc_codec_configuration));
  if (!local_stream_endpo164">int){
    pr164">intf("A2DP Source: not enough memory to create local stream endpo164">int\n");
    return 1;
  }

  avdtp_set_preferred_sampling_frequency(local_stream_endpo164">int, A2DP_SOURCE_DEMO_PREFERRED_SAMPLING_RATE);

  // Store stream enpo164">int's SEP ID, as it is used by A2DP API to indentify the stream endpo164">int
  media_tracker.local_seid = avdtp_local_seid(local_stream_endpo164">int);
  avdtp_source_register_delay_reporting_category(media_tracker.local_seid);

  // Initialize AVRCP Service
  avrcp_init();
  avrcp_register_packet_handler(&avrcp_packet_handler);
  // Initialize AVRCP Target
  avrcp_target_init();
  avrcp_target_register_packet_handler(&avrcp_target_packet_handler);

  // Initialize AVRCP Controller
  avrcp_controller_init();
  avrcp_controller_register_packet_handler(&avrcp_controller_packet_handler);

  // Initialize SDP, 
  sdp_init();

  // Create A2DP Source service record and register it with SDP
  memset(sdp_a2dp_source_service_buffer, 0, sizeof(sdp_a2dp_source_service_buffer));
  a2dp_source_create_sdp_record(sdp_a2dp_source_service_buffer, sdp_create_service_record_handle(), AVDTP_SOURCE_FEATURE_MASK_PLAYER, NULL, NULL);
  btstack_assert(de_get_len( sdp_a2dp_source_service_buffer) <= sizeof(sdp_a2dp_source_service_buffer));
  sdp_register_service(sdp_a2dp_source_service_buffer);

  // Create AVRCP Target service record and register it with SDP. We receive Category 1 commands from the headphone, e.g. play/pause
  memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
  144">u164">int16_t supported_features = AVRCP_FEATURE_MASK_CATEGORY_PLAYER_OR_RECORDER;
#ifdef AVRCP_BROWSING_ENABLED
  supported_features |= AVRCP_FEATURE_MASK_BROWSING;
#endif
  avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer, sdp_create_service_record_handle(), supported_features, NULL, NULL);
  btstack_assert(de_get_len( sdp_avrcp_target_service_buffer) <= sizeof(sdp_avrcp_target_service_buffer));
  sdp_register_service(sdp_avrcp_target_service_buffer);

  // Create AVRCP Controller service record and register it with SDP. We send Category 2 commands to the headphone, e.g. volume up/down
  memset(sdp_avrcp_controller_service_buffer, 0, sizeof(sdp_avrcp_controller_service_buffer));
  144">u164">int16_t controller_supported_features = AVRCP_FEATURE_MASK_CATEGORY_MONITOR_OR_AMPLIFIER;
  avrcp_controller_create_sdp_record(sdp_avrcp_controller_service_buffer, sdp_create_service_record_handle(), controller_supported_features, NULL, NULL);
  btstack_assert(de_get_len( sdp_avrcp_controller_service_buffer) <= sizeof(sdp_avrcp_controller_service_buffer));
  sdp_register_service(sdp_avrcp_controller_service_buffer);

  // Register Device ID (PnP) service SDP record
  memset(device_id_sdp_service_buffer, 0, sizeof(device_id_sdp_service_buffer));
  device_id_create_sdp_record(device_id_sdp_service_buffer, sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
  btstack_assert(de_get_len( device_id_sdp_service_buffer) <= sizeof(device_id_sdp_service_buffer));
  sdp_register_service(device_id_sdp_service_buffer);

  // Set local name with a template Bluetooth address, that will be automatically
  // replaced with a actual address once it is available, i.e. when BTstack boots
  // up and starts talking to a Bluetooth module.
  gap_set_local_name("A2DP Source 00:00:00:00:00:00");
  gap_discoverable_control(1);
  gap_set_class_of_device(0x200408);

  // Register for HCI events.
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  data_source = STREAM_MOD;

  // Parse human readable Bluetooth address.
  sscanf_bd_addr(device_addr_string, device_addr);

#ifdef HAVE_BTSTACK_STDIN
  btstack_stdin_setup(stdin_process);
#endif
  return 0;
}

AVRCP Browsing - Browse Media Players and Media Information

Source Code: avrcp_browsing_client.c

This example demonstrates how to use the AVRCP Controller Browsing service to browse madia players and media information on a remote AVRCP Source device.

To test with a remote device, e.g. a mobile phone, pair from the remote device with the demo, then use the UI for browsing. If HAVE_BTSTACK_STDIN is set, press SPACE on the console to show the available AVDTP and AVRCP commands.

Main Application Setup

The Listing here shows how to setup AVRCP Controller Browsing service. To announce AVRCP Controller Browsing service, you need to create corresponding SDP record and register it with the SDP service. You'll also need to register several packet handlers:

static void avrcp_browsing_controller_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void avrcp_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void a2dp_sink_packet_handler(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

#ifdef HAVE_BTSTACK_STDIN
static void stdin_process(char cmd);
#endif


164">int btstack_main(164">int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  // Initialize L2CAP.
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  a2dp_sink_init();
  a2dp_sink_register_packet_handler(&a2dp_sink_packet_handler);

  avdtp_stream_endpo164">int_t * local_stream_endpo164">int = a2dp_sink_create_stream_endpo164">int(AVDTP_AUDIO, 
    AVDTP_CODEC_SBC, media_sbc_codec_capabilities, sizeof(media_sbc_codec_capabilities), 
    media_sbc_codec_configuration, sizeof(media_sbc_codec_configuration));
  if (!local_stream_endpo164">int){
    pr164">intf("A2DP Sink: not enough memory to create local stream endpo164">int\n");
    return 1;
  }
  a2dp_local_seid = avdtp_local_seid(local_stream_endpo164">int);

  // Initialize AVRCP service.
  avrcp_init();
  // Initialize AVRCP Controller & Target Service.
  avrcp_controller_init();
  avrcp_target_init();

  avrcp_register_packet_handler(&avrcp_packet_handler);
  avrcp_controller_register_packet_handler(&avrcp_packet_handler);
  avrcp_target_register_packet_handler(&avrcp_packet_handler);

  // Initialize AVRCP Browsing Service. 
  avrcp_browsing_init();
  avrcp_browsing_controller_init();
  avrcp_browsing_target_init();

  // Register for HCI events.
  avrcp_browsing_controller_register_packet_handler(&avrcp_browsing_controller_packet_handler);
  avrcp_browsing_target_register_packet_handler(&avrcp_browsing_controller_packet_handler);
  avrcp_browsing_register_packet_handler(&avrcp_browsing_controller_packet_handler);

  // Initialize SDP. 
  sdp_init();
  // setup AVDTP sink
  memset(sdp_avdtp_sink_service_buffer, 0, sizeof(sdp_avdtp_sink_service_buffer));
  a2dp_sink_create_sdp_record(sdp_avdtp_sink_service_buffer, sdp_create_service_record_handle(), AVDTP_SINK_FEATURE_MASK_HEADPHONE, NULL, NULL);
  btstack_assert(de_get_len( sdp_avdtp_sink_service_buffer) <= sizeof(sdp_avdtp_sink_service_buffer));
  sdp_register_service(sdp_avdtp_sink_service_buffer);

  // Create AVRCP service record and register it with SDP.
  memset(sdp_avrcp_browsing_controller_service_buffer, 0, sizeof(sdp_avrcp_browsing_controller_service_buffer));

  144">u164">int16_t supported_features = AVRCP_FEATURE_MASK_CATEGORY_PLAYER_OR_RECORDER;
#ifdef AVRCP_BROWSING_ENABLED
  supported_features |= AVRCP_FEATURE_MASK_BROWSING;
#endif
  avrcp_controller_create_sdp_record(sdp_avrcp_browsing_controller_service_buffer, sdp_create_service_record_handle(), supported_features, NULL, NULL);
  btstack_assert(de_get_len( sdp_avrcp_browsing_controller_service_buffer) <= sizeof(sdp_avrcp_browsing_controller_service_buffer));
  sdp_register_service(sdp_avrcp_browsing_controller_service_buffer);

  // Set local name with a template Bluetooth address, that will be automatically
  // replaced with a actual address once it is available, i.e. when BTstack boots
  // up and starts talking to a Bluetooth module.
  gap_set_local_name("AVRCP Browsing Client 00:00:00:00:00:00");
  gap_discoverable_control(1);
  gap_set_class_of_device(0x200408);

  // Register for HCI events.
  hci_event_callback_registration.callback = &avrcp_browsing_controller_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);


#ifdef HAVE_BTSTACK_STDIN
  // Parse human readable Bluetooth address.
  sscanf_bd_addr(device_addr_string, device_addr);
  btstack_stdin_setup(stdin_process);
#endif
  pr164">intf("Starting BTstack ...\n");
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HFP AG - Audio Gateway

Source Code: hfp_ag_demo.c

This HFP Audio Gateway example demonstrates how to receive an output from a remote HFP Hands-Free (HF) unit, and, if HAVE_BTSTACK_STDIN is defined, how to control the HFP HF.

Main Application Setup

Listing here shows main application code. To run a HFP AG service you need to initialize the SDP, and to create and register HFP AG record with it. The packet_handler is used for sending commands to the HFP HF. It also receives the HFP HF's answers. The stdin_process callback allows for sending commands to the HFP HF. At the end the Bluetooth stack is started.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  sco_demo_init();

  // Request role change on reconnecting headset to always use them in slave mode
  hci_set_master_slave_policy(0);

  gap_set_local_name("HFP AG Demo 00:00:00:00:00:00");
  gap_discoverable_control(1);

  // L2CAP
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  144">u164">int16_t supported_features           =
    (1<<HFP_AGSF_ESCO_S4)           |
    (1<<HFP_AGSF_HF_INDICATORS)         |
    (1<<HFP_AGSF_CODEC_NEGOTIATION)       |
    (1<<HFP_AGSF_EXTENDED_ERROR_RESULT_CODES) |
    (1<<HFP_AGSF_ENHANCED_CALL_CONTROL)     |
    (1<<HFP_AGSF_ENHANCED_CALL_STATUS)    |
    (1<<HFP_AGSF_ABILITY_TO_REJECT_A_CALL)  |
    (1<<HFP_AGSF_IN_BAND_RING_TONE)       |
    (1<<HFP_AGSF_VOICE_RECOGNITION_FUNCTION)  |
    (1<<HFP_AGSF_ENHANCED_VOICE_RECOGNITION_STATUS) |
    (1<<HFP_AGSF_VOICE_RECOGNITION_TEXT) |
    (1<<HFP_AGSF_EC_NR_FUNCTION) |
    (1<<HFP_AGSF_THREE_WAY_CALLING);

  // HFP
  rfcomm_init();
  hfp_ag_init(rfcomm_channel_nr);
  hfp_ag_init_supported_features(supported_features);
  hfp_ag_init_codecs(sizeof(codecs), codecs);
  hfp_ag_init_ag_indicators(ag_indicators_nr, ag_indicators);
  hfp_ag_init_hf_indicators(hf_indicators_nr, hf_indicators); 
  hfp_ag_init_call_hold_services(call_hold_services_nr, call_hold_services);
  hfp_ag_set_subcriber_number_information(&subscriber_number, 1);

  // SDP Server
  sdp_init();
  memset(hfp_service_buffer, 0, sizeof(hfp_service_buffer));
  hfp_ag_create_sdp_record_with_codecs( hfp_service_buffer, sdp_create_service_record_handle(),
                      rfcomm_channel_nr, hfp_ag_service_name, 0, supported_features, sizeof(codecs), codecs);
  btstack_assert(de_get_len( hfp_service_buffer) <= sizeof(hfp_service_buffer));
  sdp_register_service(hfp_service_buffer);

  // register for HCI events and SCO packets
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
  hci_register_sco_packet_handler(&packet_handler);

  // register for HFP events
  hfp_ag_register_packet_handler(&packet_handler);

  // parse human readable Bluetooth address
  sscanf_bd_addr(device_addr_string, device_addr);

#ifdef HAVE_BTSTACK_STDIN
  btstack_stdin_setup(stdin_process);
#endif  
  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HFP HF - Hands-Free

Source Code: hfp_hs_demo.c

This HFP Hands-Free example demonstrates how to receive an output from a remote HFP audio gateway (AG), and, if HAVE_BTSTACK_STDIN is defined, how to control the HFP AG.

Main Application Setup

Listing here shows main application code. To run a HFP HF service you need to initialize the SDP, and to create and register HFP HF record with it. The packet_handler is used for sending commands to the HFP AG. It also receives the HFP AG's answers. The stdin_process callback allows for sending commands to the HFP AG. At the end the Bluetooth stack is started.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  // Init protocols
  // init L2CAP
  l2cap_init();
  rfcomm_init();
  sdp_init();
#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // Init profiles
  144">u164">int16_t hf_supported_features      =
    (1<<HFP_HFSF_ESCO_S4)         |
    (1<<HFP_HFSF_CLI_PRESENTATION_CAPABILITY) |
    (1<<HFP_HFSF_HF_INDICATORS)     |
    (1<<HFP_HFSF_CODEC_NEGOTIATION)   |
    (1<<HFP_HFSF_ENHANCED_CALL_STATUS)  |
    (1<<HFP_HFSF_VOICE_RECOGNITION_FUNCTION)  |
    (1<<HFP_HFSF_ENHANCED_VOICE_RECOGNITION_STATUS) |
    (1<<HFP_HFSF_VOICE_RECOGNITION_TEXT) |
    (1<<HFP_HFSF_EC_NR_FUNCTION) |
    (1<<HFP_HFSF_REMOTE_VOLUME_CONTROL);

  hfp_hf_init(rfcomm_channel_nr);
  hfp_hf_init_supported_features(hf_supported_features);
  hfp_hf_init_hf_indicators(sizeof(indicators)/sizeof(144">u164">int16_t), indicators);
  hfp_hf_init_codecs(sizeof(codecs), codecs);
  hfp_hf_register_packet_handler(hfp_hf_packet_handler);


  // Configure SDP

  // - Create and register HFP HF service record
  memset(hfp_service_buffer, 0, sizeof(hfp_service_buffer));
  hfp_hf_create_sdp_record_with_codecs(hfp_service_buffer, sdp_create_service_record_handle(),
               rfcomm_channel_nr, hfp_hf_service_name, hf_supported_features, sizeof(codecs), codecs);
  btstack_assert(de_get_len( hfp_service_buffer) <= sizeof(hfp_service_buffer));
  sdp_register_service(hfp_service_buffer);

  // Configure GAP - discovery / connection

  // - Set local name with a template Bluetooth address, that will be automatically
  //   replaced with an actual address once it is available, i.e. when BTstack boots
  //   up and starts talking to a Bluetooth module.
  gap_set_local_name("HFP HF Demo 00:00:00:00:00:00");

  // - Allow to show up in Bluetooth inquiry
  gap_discoverable_control(1);

  // - Set Class of Device - Service Class: Audio, Major Device Class: Audio, Minor: Hands-Free device
  gap_set_class_of_device(0x200408);

  // - Allow for role switch in general and sniff mode
  gap_set_default_link_policy_settings( LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE );

  // - Allow for role switch on outgoing connections - this allows HFP AG, e.g. smartphone, to become master when we re-connect to it
  gap_set_allow_role_switch(true);

  // Register for HCI events and SCO packets
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
  hci_register_sco_packet_handler(&hci_packet_handler);


  // Init SCO / HFP audio processing
  sco_demo_init();

#ifdef HAVE_BTSTACK_STDIN
  // parse human readable Bluetooth address
  sscanf_bd_addr(device_addr_string, device_addr);
  btstack_stdin_setup(stdin_process);
#endif

  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HSP AG - Audio Gateway

Source Code: hsp_ag_demo.c

This example implements a HSP Audio Gateway device that sends and receives audio signal over HCI SCO. It demonstrates how to receive an output from a remote headset (HS), and, if HAVE_BTSTACK_STDIN is defined, how to control the HS.

Audio Transfer Setup

A pre-computed sine wave (160Hz) is used as the input audio signal. 160 Hz. To send and receive an audio signal, ENABLE_SCO_OVER_HCI has to be defined.

Tested working setups:

Main Application Setup

Listing here shows main application code. To run a HSP Audio Gateway service you need to initialize the SDP, and to create and register HSP AG record with it. In this example, the SCO over HCI is used to receive and send an audio signal.

Two packet handlers are registered:

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  sco_demo_init();
  sco_demo_set_codec(HFP_CODEC_CVSD);

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  sdp_init();

  memset((u164">int8_t *)hsp_service_buffer, 0, sizeof(hsp_service_buffer));
  hsp_ag_create_sdp_record(hsp_service_buffer, sdp_create_service_record_handle(), rfcomm_channel_nr, hsp_ag_service_name);
  btstack_assert(de_get_len( hsp_service_buffer) <= sizeof(hsp_service_buffer));
  sdp_register_service(hsp_service_buffer);

  rfcomm_init();

  hsp_ag_init(rfcomm_channel_nr);
  hsp_ag_register_packet_handler(&packet_handler);

  // register for SCO packets
  hci_register_sco_packet_handler(&packet_handler);

  // parse human readable Bluetooth address
  sscanf_bd_addr(device_addr_string, device_addr);

#ifdef HAVE_BTSTACK_STDIN
  btstack_stdin_setup(stdin_process);
#endif

  gap_set_local_name(device_name);
  gap_discoverable_control(1);
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_set_class_of_device(0x400204);

  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HSP HS - Headset

Source Code: hsp_hs_demo.c

This example implements a HSP Headset device that sends and receives audio signal over HCI SCO. It demonstrates how to receive an output from a remote audio gateway (AG), and, if HAVE_BTSTACK_STDIN is defined, how to control the AG.

Audio Transfer Setup

A pre-computed sine wave (160Hz) is used as the input audio signal. 160 Hz. To send and receive an audio signal, ENABLE_SCO_OVER_HCI has to be defined.

Tested working setups:

Main Application Setup

Listing here shows main application code. To run a HSP Headset service you need to initialize the SDP, and to create and register HSP HS record with it. In this example, the SCO over HCI is used to receive and send an audio signal.

Two packet handlers are registered:

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  sco_demo_init();
  sco_demo_set_codec(HFP_CODEC_CVSD);

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  sdp_init();
  memset(hsp_service_buffer, 0, sizeof(hsp_service_buffer));
  hsp_hs_create_sdp_record(hsp_service_buffer, sdp_create_service_record_handle(), rfcomm_channel_nr, hsp_hs_service_name, 0);
  btstack_assert(de_get_len( hsp_service_buffer) <= sizeof(hsp_service_buffer));
  sdp_register_service(hsp_service_buffer);

  rfcomm_init();

  hsp_hs_init(rfcomm_channel_nr);

  // register for HCI events and SCO packets
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
  hci_register_sco_packet_handler(&packet_handler);

  // register for HSP events
  hsp_hs_register_packet_handler(packet_handler);

#ifdef HAVE_BTSTACK_STDIN
  btstack_stdin_setup(stdin_process);
#endif

  gap_set_local_name("HSP HS Demo 00:00:00:00:00:00");
  gap_discoverable_control(1);
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_set_class_of_device(0x240404);

  // Parse human readable Bluetooth address.
  sscanf_bd_addr(device_addr_string, device_addr);

  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

Audio Driver - Play Sine

Source Code: audio_duplex.c

Play sine to test and validate audio output with simple wave form.

Audio Driver - Play 80's MOD Song

Source Code: mod_player.c

Audio Driver - Forward Audio from Source to Sink

Source Code: audio_duplex.c

SPP Server - Heartbeat Counter over RFCOMM

Source Code: spp_counter.c

The Serial port profile (SPP) is widely used as it provides a serial port over Bluetooth. The SPP counter example demonstrates how to setup an SPP service, and provide a periodic timer over RFCOMM.

Note: To test, please run the spp_counter example, and then pair from a remote device, and open the Virtual Serial Port.

SPP Service Setup

To provide an SPP service, the L2CAP, RFCOMM, and SDP protocol layers are required. After setting up an RFCOMM service with channel nubmer RFCOMM_SERVER_CHANNEL, an SDP record is created and registered with the SDP server. Example code for SPP service setup is provided in Listing here. The SDP record created by function spp_create_sdp_record consists of a basic SPP definition that uses the provided RFCOMM channel ID and service name. For more details, please have a look at it in \path{src/sdp_util.c}. The SDP record is created on the fly in RAM and is deterministic. To preserve valuable RAM, the result could be stored as constant data inside the ROM.

static void spp_service_setup(void){

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  rfcomm_init();
  rfcomm_register_service(packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff);  // reserved channel, mtu limited by l2cap

  // init SDP, create record for SPP and register with SDP
  sdp_init();
  memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
  spp_create_sdp_record(spp_service_buffer, sdp_create_service_record_handle(), RFCOMM_SERVER_CHANNEL, "SPP Counter");
  btstack_assert(de_get_len( spp_service_buffer) <= sizeof(spp_service_buffer));
  sdp_register_service(spp_service_buffer);
}

Periodic Timer Setup

The heartbeat handler increases the real counter every second, and sends a text string with the counter value, as shown in Listing here.

static btstack_timer_source_t heartbeat;
static char lineBuffer[30];
static void  heartbeat_handler(struct btstack_timer_source *ts){
  static 164">int counter = 0;

  if (rfcomm_channel_id){
    snpr164">intf(lineBuffer, sizeof(lineBuffer), "BTstack counter %04u\n", ++counter);
    pr164">intf("%s", lineBuffer);

    rfcomm_request_can_send_now_event(rfcomm_channel_id);
  }

  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

static void one_shot_timer_setup(void){
  // set one-shot timer
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);
}

Bluetooth Logic

The Bluetooth logic is implemented within the packet handler, see Listing here. In this example, the following events are passed sequentially:

Upon receiving HCI_EVENT_PIN_CODE_REQUEST event, we need to handle authentication. Here, we use a fixed PIN code "0000".

When HCI_EVENT_USER_CONFIRMATION_REQUEST is received, the user will be asked to accept the pairing request. If the IO capability is set to SSP_IO_CAPABILITY_DISPLAY_YES_NO, the request will be automatically accepted.

The RFCOMM_EVENT_INCOMING_CONNECTION event indicates an incoming connection. Here, the connection is accepted. More logic is need, if you want to handle connections from multiple clients. The incoming RFCOMM connection event contains the RFCOMM channel number used during the SPP setup phase and the newly assigned RFCOMM channel ID that is used by all BTstack commands and events.

If RFCOMM_EVENT_CHANNEL_OPENED event returns status greater then 0, then the channel establishment has failed (rare case, e.g., client crashes). On successful connection, the RFCOMM channel ID and MTU for this channel are made available to the heartbeat counter. After opening the RFCOMM channel, the communication between client and the application takes place. In this example, the timer handler increases the real counter every second.

RFCOMM_EVENT_CAN_SEND_NOW indicates that it's possible to send an RFCOMM packet on the rfcomm_cid that is include

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);

...
        case HCI_EVENT_PIN_CODE_REQUEST:
          // inform about pin code request
          pr164">intf("Pin code request - using '0000'\n");
          hci_event_pin_code_request_get_bd_addr(packet, event_addr);
          gap_pin_code_response(event_addr, "0000");
          break;

        case HCI_EVENT_USER_CONFIRMATION_REQUEST:
          // ssp: inform about user confirmation request
          pr164">intf("SSP User Confirmation Request with numeric value '%06"PRIu32"'\n", little_endian_read_32(packet, 8));
          pr164">intf("SSP User Confirmation Auto accept\n");
          break;

        case RFCOMM_EVENT_INCOMING_CONNECTION:
          rfcomm_event_incoming_connection_get_bd_addr(packet, event_addr);
          rfcomm_channel_nr = rfcomm_event_incoming_connection_get_server_channel(packet);
          rfcomm_channel_id = rfcomm_event_incoming_connection_get_rfcomm_cid(packet);
          pr164">intf("RFCOMM channel %u requested for %s\n", rfcomm_channel_nr, bd_addr_to_str(event_addr));
          rfcomm_accept_connection(rfcomm_channel_id);
          break;

        case RFCOMM_EVENT_CHANNEL_OPENED:
          if (rfcomm_event_channel_opened_get_status(packet)) {
            pr164">intf("RFCOMM channel open failed, status 0x%02x\n", rfcomm_event_channel_opened_get_status(packet));
          } else {
            rfcomm_channel_id = rfcomm_event_channel_opened_get_rfcomm_cid(packet);
            mtu = rfcomm_event_channel_opened_get_max_frame_size(packet);
            pr164">intf("RFCOMM channel open succeeded. New RFCOMM Channel ID %u, max frame size %u\n", rfcomm_channel_id, mtu);
          }
          break;
        case RFCOMM_EVENT_CAN_SEND_NOW:
          rfcomm_send(rfcomm_channel_id, (u164">int8_t*) lineBuffer, (144">u164">int16_t) strlen(lineBuffer));  
          break;

...
}

SPP Server - RFCOMM Flow Control

Source Code: spp_flowcontrol.c

This example adds explicit flow control for incoming RFCOMM data to the SPP heartbeat counter example. We will highlight the changes compared to the SPP counter example.

SPP Service Setup

Listing here shows how to provide one initial credit during RFCOMM service initialization. Please note that providing a single credit effectively reduces the credit-based (sliding window) flow control to a stop-and-wait flow control that limits the data throughput substantially.

static void spp_service_setup(void){

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // init L2CAP
  l2cap_init();

  // init RFCOMM
  rfcomm_init();
  // reserved channel, mtu limited by l2cap, 1 credit
  rfcomm_register_service_with_initial_credits(&packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff, 1);

  // init SDP, create record for SPP and register with SDP
  sdp_init();
  memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
  spp_create_sdp_record(spp_service_buffer, sdp_create_service_record_handle(), 1, "SPP Counter");
  btstack_assert(de_get_len( spp_service_buffer) <= sizeof(spp_service_buffer));
  sdp_register_service(spp_service_buffer);
}

Periodic Timer Setup

Explicit credit management is recommended when received RFCOMM data cannot be processed immediately. In this example, delayed processing of received data is simulated with the help of a periodic timer as follows. When the packet handler receives a data packet, it does not provide a new credit, it sets a flag instead, see Listing here. If the flag is set, a new credit will be granted by the heartbeat handler, introducing a delay of up to 1 second. The heartbeat handler code is shown in Listing here.

static void  heartbeat_handler(struct btstack_timer_source *ts){
  if (rfcomm_send_credit){
    rfcomm_grant_credits(rfcomm_channel_id, 1);
    rfcomm_send_credit = 0;
  }
  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

// Bluetooth logic
static void packet_handler (u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size){
...
    case RFCOMM_DATA_PACKET:
      for (i=0;i<size;i++){
        putchar(packet[i]);
      };
      putchar('\n');
      rfcomm_send_credit = 1;
      break;
...
}

PAN - lwIP HTTP and DHCP Server

Source Code: pan_lwip_http_server.c

Bluetooth PAN is mainly used for Internet Tethering, where e.g. a mobile phone provides internet connection to a laptop or a tablet.

Instead of regular internet access, it's also possible to provide a Web app on a Bluetooth device, e.g. for configuration or maintenance. For some device, this can be a more effective way to provide an interface compared to dedicated smartphone applications (for Android and iOS).

Before iOS 11, accessing an HTTP server via Bluetooth PAN was not supported on the iPhone, but on iPod and iPad. With iOS 11, this works as expected.

After pairing your device, please open the URL http://192.168.7.1 in your web browser.

Packet Handler

All BNEP events are handled in the platform/bnep_lwip.c BNEP-LWIP Adapter. Here, we only print status information and handle pairing requests.

PAN BNEP Setup

DHCP Server Configuration

Large File Download

DHCP Server Setup

Main

BNEP/PANU (Linux only)

Source Code: panu_demo.c

BNEP_EVENT_CHANNEL_OPENED is received after a BNEP connection was established or or when the connection fails. The status field returns the error code.

BNEP_EVENT_CHANNEL_CLOSED is received when the connection gets closed.

Listing here shows the setup of the PAN setup

Listing here shows the DHCP Server configuration for network 192.168.7.0/8

Listing here Shows how a configurable test file for performance tests is generated on the fly. The filename is the number of bytes to generate, e.g. /1048576.txt results in a 1MB file.

Listing here shows the setup of the lwIP network stack and starts the DHCP Server

Setup the lwIP network and PAN NAP

This example implements both a PANU client and a server. In server mode, it sets up a BNEP server and registers a PANU SDP record and waits for incoming connections. In client mode, it connects to a remote device, does an SDP Query to identify the PANU service and initiates a BNEP connection.

Note: currently supported only on Linux and provides a TAP network interface which you can configure yourself.

To enable client mode, uncomment ENABLE_PANU_CLIENT below.

Main application configuration

In the application configuration, L2CAP and BNEP are initialized and a BNEP service, for server mode, is registered, before the Bluetooth stack gets started, as shown in Listing here.

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void handle_sdp_client_query_result(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);
static void network_send_packet_callback(const u164">int8_t * packet, 144">u164">int16_t size);

static void panu_setup(void){

  // Initialize L2CAP
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // init SDP, create record for PANU and register with SDP
  sdp_init();
  memset(panu_sdp_record, 0, sizeof(panu_sdp_record));
  144">u164">int16_t network_packet_types[] = { NETWORK_TYPE_IPv4, NETWORK_TYPE_ARP, 0};  // 0 as end of list

  // Initialise BNEP
  bnep_init();
  // Minimum L2CAP MTU for bnep is 1691 bytes
#ifdef ENABLE_PANU_CLIENT
  bnep_register_service(packet_handler, BLUETOOTH_SERVICE_CLASS_PANU, 1691);
  // PANU
  pan_create_panu_sdp_record(panu_sdp_record, sdp_create_service_record_handle(), network_packet_types, NULL, NULL, BNEP_SECURITY_NONE);
#else
  bnep_register_service(packet_handler, BLUETOOTH_SERVICE_CLASS_NAP, 1691);
  // NAP Network Access Type: Other, 1 MB/s
  pan_create_nap_sdp_record(panu_sdp_record, sdp_create_service_record_handle(), network_packet_types, NULL, NULL, BNEP_SECURITY_NONE, PAN_NET_ACCESS_TYPE_OTHER, 1000000, NULL, NULL);
#endif
  btstack_assert(de_get_len( panu_sdp_record) <= sizeof(panu_sdp_record));
  sdp_register_service(panu_sdp_record);

  // Initialize network 164">interface
  btstack_network_init(&network_send_packet_callback);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

SDP parser callback

The SDP parsers retrieves the BNEP PAN UUID as explained in Section [on SDP BNEP Query example](#sec:sdpbnepqueryExample}.

Packet Handler

The packet handler responds to various HCI Events.

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size)
{
...
  switch (packet_type) {
        case HCI_EVENT_PACKET:
      event = hci_event_packet_get_type(packet);
      switch (event) {      
#ifdef ENABLE_PANU_CLIENT
        case BTSTACK_EVENT_STATE:
          if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
            pr164">intf("Start SDP BNEP query for remote PAN Network Access Po164">int (NAP).\n");
            sdp_client_query_uuid16(&handle_sdp_client_query_result, remote_addr, BLUETOOTH_SERVICE_CLASS_NAP);
          }
          break;
#endif
...

                case BNEP_EVENT_CHANNEL_OPENED:
          if (bnep_event_channel_opened_get_status(packet)) {
            pr164">intf("BNEP channel open failed, status 0x%02x\n", bnep_event_channel_opened_get_status(packet));
          } else {
            bnep_cid  = bnep_event_channel_opened_get_bnep_cid(packet);
            uuid_source = bnep_event_channel_opened_get_source_uuid(packet);
            uuid_dest   = bnep_event_channel_opened_get_destination_uuid(packet);
            mtu     = bnep_event_channel_opened_get_mtu(packet);
            bnep_event_channel_opened_get_remote_address(packet, event_addr);
            pr164">intf("BNEP connection open succeeded to %s source UUID 0x%04x dest UUID: 0x%04x, max frame size %u\n", bd_addr_to_str(event_addr), uuid_source, uuid_dest, mtu);

            gap_local_bd_addr(local_addr);
            btstack_network_up(local_addr);
            pr164">intf("Network Interface %s activated\n", btstack_network_get_name());
          }
                    break;

        case BNEP_EVENT_CHANNEL_TIMEOUT:
          pr164">intf("BNEP channel timeout! Channel will be closed\n");
          break;

        case BNEP_EVENT_CHANNEL_CLOSED:
          pr164">intf("BNEP channel closed\n");
          btstack_network_down();
          break;

        case BNEP_EVENT_CAN_SEND_NOW:
          if (network_buffer_len > 0) {
            bnep_send(bnep_cid, (u164">int8_t*) network_buffer, network_buffer_len);
            network_buffer_len = 0;
            btstack_network_packet_sent();
          }
          break;

        default:
          break;
      }
      break;

    case BNEP_DATA_PACKET:
      // Write out the ethernet frame to the network 164">interface
      btstack_network_process_packet(packet, size);
      break;

    default:
      break;
  }
}

When BTSTACK_EVENT_STATE with state HCI_STATE_WORKING is received and the example is started in client mode, the remote SDP BNEP query is started.

BNEP_EVENT_CHANNEL_OPENED is received after a BNEP connection was established or or when the connection fails. The status field returns the error code.

The TAP network interface is then configured. A data source is set up and registered with the run loop to receive Ethernet packets from the TAP interface.

The event contains both the source and destination UUIDs, as well as the MTU for this connection and the BNEP Channel ID, which is used for sending Ethernet packets over BNEP.

If there is a timeout during the connection setup, BNEP_EVENT_CHANNEL_TIMEOUT will be received and the BNEP connection will be closed

BNEP_EVENT_CHANNEL_CLOSED is received when the connection gets closed.

BNEP_EVENT_CAN_SEND_NOW indicates that a new packet can be send. This triggers the send of a stored network packet. The tap datas source can be enabled again

Ethernet packets from the remote device are received in the packet handler with type BNEP_DATA_PACKET. It is forwarded to the TAP interface.

Network packet handler

A pointer to the network packet is stored and a BNEP_EVENT_CAN_SEND_NOW requested

static void network_send_packet_callback(const uint8_t * packet, uint16_t size){
  network_buffer = packet;
  network_buffer_len = size;
  bnep_request_can_send_now_event(bnep_cid);
}

HID Keyboard Classic

Source Code: hid_keyboard_demo.c

This HID Device example demonstrates how to implement an HID keyboard. Without a HAVE_BTSTACK_STDIN, a fixed demo text is sent If HAVE_BTSTACK_STDIN is defined, you can type from the terminal

Main Application Setup

Listing here shows main application code. To run a HID Device service you need to initialize the SDP, and to create and register HID Device record with it. At the end the Bluetooth stack is started.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  // allow to get found by inquiry
  gap_discoverable_control(1);
  // use Limited Discoverable Mode; Peripheral; Keyboard as CoD
  gap_set_class_of_device(0x2540);
  // set local name to be identified - zeroes will be replaced by actual BD ADDR
  gap_set_local_name("HID Keyboard Demo 00:00:00:00:00:00");
  // allow for role switch in general and sniff mode
  gap_set_default_link_policy_settings( LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE );
  // allow for role switch on outgoing connections - this allow HID Host to become master when we re-connect to it
  gap_set_allow_role_switch(true);

  // L2CAP
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // SDP Server
  sdp_init();
  memset(hid_service_buffer, 0, sizeof(hid_service_buffer));

  u164">int8_t hid_virtual_cable = 0;
  u164">int8_t hid_remote_wake = 1;
  u164">int8_t hid_reconnect_initiate = 1;
  u164">int8_t hid_normally_connectable = 1;

  hid_sdp_record_t hid_params = {
    // hid sevice subclass 2540 Keyboard, hid counntry code 33 US
    0x2540, 33, 
    hid_virtual_cable, hid_remote_wake, 
    hid_reconnect_initiate, hid_normally_connectable,
    hid_boot_device,
    host_max_latency, host_min_timeout,
    3200,
    hid_descriptor_keyboard,
    sizeof(hid_descriptor_keyboard),
    hid_device_name
  };

  hid_create_sdp_record(hid_service_buffer, sdp_create_service_record_handle(), &hid_params);
  btstack_assert(de_get_len( hid_service_buffer) <= sizeof(hid_service_buffer));
  sdp_register_service(hid_service_buffer);

  // See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID
  // device info: BlueKitchen GmbH, product 1, version 1
  device_id_create_sdp_record(device_id_sdp_service_buffer, sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
  btstack_assert(de_get_len( device_id_sdp_service_buffer) <= sizeof(device_id_sdp_service_buffer));
  sdp_register_service(device_id_sdp_service_buffer);

  // HID Device
  hid_device_init(hid_boot_device, sizeof(hid_descriptor_keyboard), hid_descriptor_keyboard);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for HID events
  hid_device_register_packet_handler(&packet_handler);

#ifdef HAVE_BTSTACK_STDIN
  sscanf_bd_addr(device_addr_string, device_addr);
  btstack_stdin_setup(stdin_process);
#endif

  btstack_ring_buffer_init(&send_buffer, send_buffer_storage, sizeof(send_buffer_storage));

  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HID Mouse Classic

Source Code: hid_mouse_demo.c

This HID Device example demonstrates how to implement an HID keyboard. Without a HAVE_BTSTACK_STDIN, a fixed demo text is sent If HAVE_BTSTACK_STDIN is defined, you can type from the terminal

Main Application Setup

Listing here shows main application code. To run a HID Device service you need to initialize the SDP, and to create and register HID Device record with it. At the end the Bluetooth stack is started.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  // allow to get found by inquiry
  gap_discoverable_control(1);
  // use Limited Discoverable Mode; Peripheral; Po164">inting Device as CoD
  gap_set_class_of_device(0x2580);
  // set local name to be identified - zeroes will be replaced by actual BD ADDR
  gap_set_local_name("HID Mouse Demo 00:00:00:00:00:00");
  // allow for role switch in general and sniff mode
  gap_set_default_link_policy_settings( LM_LINK_POLICY_ENABLE_ROLE_SWITCH | LM_LINK_POLICY_ENABLE_SNIFF_MODE );
  // allow for role switch on outgoing connections - this allow HID Host to become master when we re-connect to it
  gap_set_allow_role_switch(true);

  // L2CAP
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // SDP Server
  sdp_init();

  u164">int8_t hid_virtual_cable = 0;
  u164">int8_t hid_remote_wake = 1;
  u164">int8_t hid_reconnect_initiate = 1;
  u164">int8_t hid_normally_connectable = 1;

  hid_sdp_record_t hid_params = {
    // hid sevice subclass 2580 Mouse, hid counntry code 33 US
    0x2580, 33, 
    hid_virtual_cable, hid_remote_wake, 
    hid_reconnect_initiate, hid_normally_connectable,
    hid_boot_device, 
    0xFFFF, 0xFFFF, 3200,
    hid_descriptor_mouse_boot_mode,
    sizeof(hid_descriptor_mouse_boot_mode), 
    hid_device_name
  };

  memset(hid_service_buffer, 0, sizeof(hid_service_buffer));
  hid_create_sdp_record(hid_service_buffer, sdp_create_service_record_handle(), &hid_params);
  btstack_assert(de_get_len( hid_service_buffer) <= sizeof(hid_service_buffer));
  sdp_register_service(hid_service_buffer);

  // See https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers if you don't have a USB Vendor ID and need a Bluetooth Vendor ID
  // device info: BlueKitchen GmbH, product 2, version 1
  device_id_create_sdp_record(device_id_sdp_service_buffer, sdp_create_service_record_handle(), DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 2, 1);
  btstack_assert(de_get_len( device_id_sdp_service_buffer) <= sizeof(device_id_sdp_service_buffer));
  sdp_register_service(device_id_sdp_service_buffer);

  // HID Device
  hid_device_init(hid_boot_device, sizeof(hid_descriptor_mouse_boot_mode), hid_descriptor_mouse_boot_mode);
  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for HID
  hid_device_register_packet_handler(&packet_handler);

#ifdef HAVE_BTSTACK_STDIN
  btstack_stdin_setup(stdin_process);
#endif
  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

HID Host Classic

Source Code: hid_host_demo.c

This example implements a HID Host. For now, it connects to a fixed device. It will connect in Report protocol mode if this mode is supported by the HID Device, otherwise it will fall back to BOOT protocol mode.

Main application configuration

In the application configuration, L2CAP and HID host are initialized, and the link policies are set to allow sniff mode and role change.

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);

static void hid_host_setup(void){

  // Initialize L2CAP
  l2cap_init();

#ifdef ENABLE_BLE
  // Initialize LE Security Manager. Needed for cross-transport key derivation
  sm_init();
#endif

  // Initialize HID Host
  hid_host_init(hid_descriptor_storage, sizeof(hid_descriptor_storage));
  hid_host_register_packet_handler(packet_handler);

  // Allow sniff mode requests by HID device and support role switch
  gap_set_default_link_policy_settings(LM_LINK_POLICY_ENABLE_SNIFF_MODE | LM_LINK_POLICY_ENABLE_ROLE_SWITCH);

  // try to become master on incoming connections
  hci_set_master_slave_policy(HCI_ROLE_MASTER);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // Disable stdout buffering
  setvbuf(stdin, NULL, _IONBF, 0);
}

HID Report Handler

Use BTstack's compact HID Parser to process incoming HID Report in Report protocol mode. Iterate over all fields and process fields with usage page = 0x07 / Keyboard Check if SHIFT is down and process first character (don't handle multiple key presses)

Packet Handler

The packet handler responds to various HID events.

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size)
{
...
  switch (packet_type) {
        case HCI_EVENT_PACKET:
      event = hci_event_packet_get_type(packet);

      switch (event) {      
#ifndef HAVE_BTSTACK_STDIN
        case BTSTACK_EVENT_STATE:
          if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
            status = hid_host_connect(remote_addr, hid_host_report_mode, &hid_host_cid);
            if (status != ERROR_CODE_SUCCESS){
              pr164">intf("HID host connect failed, status 0x%02x.\n", status);
            }
          }
          break;
#endif
...
        case HCI_EVENT_HID_META:
          switch (hci_event_hid_meta_get_subevent_code(packet)){

            case HID_SUBEVENT_INCOMING_CONNECTION:
              // There is an incoming connection: we can accept it or decline it.
              // The hid_host_report_mode in the hid_host_accept_connection function 
              // allows the application to request a protocol mode. 
              // For available protocol modes, see hid_protocol_mode_t in btstack_hid.h file. 
              hid_host_accept_connection(hid_subevent_incoming_connection_get_hid_cid(packet), hid_host_report_mode);
              break;

            case HID_SUBEVENT_CONNECTION_OPENED:
              // The status field of this event indicates if the control and 164">interrupt
              // connections were opened successfully.
              status = hid_subevent_connection_opened_get_status(packet);
              if (status != ERROR_CODE_SUCCESS) {
                pr164">intf("Connection failed, status 0x%02x\n", status);
                app_state = APP_IDLE;
                hid_host_cid = 0;
                return;
              }
              app_state = APP_CONNECTED;
              hid_host_descriptor_available = false;
              hid_host_cid = hid_subevent_connection_opened_get_hid_cid(packet);
              pr164">intf("HID Host connected.\n");
              break;

            case HID_SUBEVENT_DESCRIPTOR_AVAILABLE:
              // This event will follows HID_SUBEVENT_CONNECTION_OPENED event. 
              // For incoming connections, i.e. HID Device initiating the connection,
              // the HID_SUBEVENT_DESCRIPTOR_AVAILABLE is delayed, and some HID  
              // reports may be received via HID_SUBEVENT_REPORT event. It is up to 
              // the application if these reports should be buffered or ignored until 
              // the HID descriptor is available.
              status = hid_subevent_descriptor_available_get_status(packet);
              if (status == ERROR_CODE_SUCCESS){
                hid_host_descriptor_available = true;
                pr164">intf("HID 124">Descriptor available, please start typing.\n");
              } else {
                pr164">intf("Cannot handle input report, HID 124">Descriptor is not available, status 0x%02x\n", status);
              }
              break;

            case HID_SUBEVENT_REPORT:
              // Handle input report.
              if (hid_host_descriptor_available){
                hid_host_handle_164">interrupt_report(hid_subevent_report_get_report(packet), hid_subevent_report_get_report_len(packet));
              } else {
                pr164">intf_hexdump(hid_subevent_report_get_report(packet), hid_subevent_report_get_report_len(packet));
              }
              break;

            case HID_SUBEVENT_SET_PROTOCOL_RESPONSE:
              // For incoming connections, the library will set the protocol mode of the
              // HID Device as requested in the call to hid_host_accept_connection. The event 
              // reports the result. For connections initiated by calling hid_host_connect, 
              // this event will occur only if the established report mode is boot mode.
              status = hid_subevent_set_protocol_response_get_handshake_status(packet);
              if (status != HID_HANDSHAKE_PARAM_TYPE_SUCCESSFUL){
                pr164">intf("Error set protocol, status 0x%02x\n", status);
                break;
              }
              switch ((hid_protocol_mode_t)hid_subevent_set_protocol_response_get_protocol_mode(packet)){
                case HID_PROTOCOL_MODE_BOOT:
                  pr164">intf("Protocol mode set: BOOT.\n");
                  break;  
                case HID_PROTOCOL_MODE_REPORT:
                  pr164">intf("Protocol mode set: REPORT.\n");
                  break;
                default:
                  pr164">intf("Unknown protocol mode.\n");
                  break; 
              }
              break;

            case HID_SUBEVENT_CONNECTION_CLOSED:
              // The connection was closed.
              hid_host_cid = 0;
              hid_host_descriptor_available = false;
              pr164">intf("HID Host disconnected.\n");
              break;

            default:
              break;
          }
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

When BTSTACK_EVENT_STATE with state HCI_STATE_WORKING is received and the example is started in client mode, the remote SDP HID query is started.

HID Keyboard LE

Source Code: hog_keyboard_demo.c

HID Mouse LE

Source Code: hog_mouse_demo.c

HID Boot Host LE

Source Code: hog_boot_host_demo.c

This example implements a minimal HID-over-GATT Boot Host. It scans for LE HID devices, connects to it, discovers the Characteristics relevant for the HID Service and enables Notifications on them. It then dumps all Boot Keyboard and Mouse Input Reports

HOG Boot Keyboard Handler

Boot Keyboard Input Report contains a report of format [ modifier, reserved, 6 x usage for key 1..6 from keyboard usage] Track new usages, map key usage to actual character and simulate terminal

HOG Boot Mouse Handler

Boot Mouse Input Report contains a report of format [ buttons, dx, dy, dz = scroll wheel] Decode packet and print on stdout

@param packet_type @param channel @param packet @param size

Test if advertisement contains HID UUID

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
...
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      event = hci_event_packet_get_type(packet);
      switch (event) {
        case BTSTACK_EVENT_STATE:
          if (btstack_event_state_get_state(packet) != HCI_STATE_WORKING) break;
          btstack_assert(app_state == W4_WORKING);
          hog_start_connect();
          break;
        case GAP_EVENT_ADVERTISING_REPORT:
          if (app_state != W4_HID_DEVICE_FOUND) break;
          if (adv_event_contains_hid_service(packet) == false) break;
          // stop scan
          gap_stop_scan();
          // store remote device address and type
          gap_event_advertising_report_get_address(packet, remote_device.addr);
          remote_device.addr_type = gap_event_advertising_report_get_address_type(packet);
          // connect
          pr164">intf("Found, connect to device with %s address %s ...\n", remote_device.addr_type == 0 ? "public" : "random" , bd_addr_to_str(remote_device.addr));
          hog_connect();
          break;
        case HCI_EVENT_DISCONNECTION_COMPLETE:
          if (app_state != READY) break;

          connection_handle = HCI_CON_HANDLE_INVALID;
          switch (app_state){
            case READY:
              pr164">intf("\nDisconnected, try to reconnect...\n");
              app_state = W4_TIMEOUT_THEN_RECONNECT;
              break;
            default:
              pr164">intf("\nDisconnected, start over...\n");
              app_state = W4_TIMEOUT_THEN_SCAN;
              break;
          }
          // set timer
          btstack_run_loop_set_timer(&connection_timer, 100);
          btstack_run_loop_set_timer_handler(&connection_timer, &hog_reconnect_timeout);
          btstack_run_loop_add_timer(&connection_timer);
          break;
        case HCI_EVENT_META_GAP:
          // wait for connection complete
          if (hci_event_gap_meta_get_subevent_code(packet) != GAP_SUBEVENT_LE_CONNECTION_COMPLETE) break;
          if (app_state != W4_CONNECTED) return;
          btstack_run_loop_remove_timer(&connection_timer);
          connection_handle = gap_subevent_le_connection_complete_get_connection_handle(packet);
          // request security
          app_state = W4_ENCRYPTED;
          sm_request_pairing(connection_handle);
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

HCI packet handler

The SM packet handler receives Security Manager Events required for pairing. It also receives events generated during Identity Resolving see Listing here.

static void sm_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  bool connect_to_service = false;

  switch (hci_event_packet_get_type(packet)) {
    case SM_EVENT_JUST_WORKS_REQUEST:
      pr164">intf("Just works requested\n");
      sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
      break;
    case SM_EVENT_NUMERIC_COMPARISON_REQUEST:
      pr164">intf("Confirming numeric comparison: %"PRIu32"\n", sm_event_numeric_comparison_request_get_passkey(packet));
      sm_numeric_comparison_confirm(sm_event_passkey_display_number_get_handle(packet));
      break;
    case SM_EVENT_PASSKEY_DISPLAY_NUMBER:
      pr164">intf("Display Passkey: %"PRIu32"\n", sm_event_passkey_display_number_get_passkey(packet));
      break;
    case SM_EVENT_PAIRING_COMPLETE:
      switch (sm_event_pairing_complete_get_status(packet)){
        case ERROR_CODE_SUCCESS:
          pr164">intf("Pairing complete, success\n");
          connect_to_service = true;
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          pr164">intf("Pairing failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          pr164">intf("Pairing failed, disconnected\n");
          break;
        case ERROR_CODE_AUTHENTICATION_FAILURE:
          pr164">intf("Pairing failed, reason = %u\n", sm_event_pairing_complete_get_reason(packet));
          break;
        default:
          break;
      }
      break;
    case SM_EVENT_REENCRYPTION_COMPLETE:
      pr164">intf("Re-encryption complete, success\n");
      connect_to_service = true;
      break;
    default:
      break;
  }

  if (connect_to_service){
    // continue - query primary services
    pr164">intf("Search for HID service.\n");
    app_state = W4_HID_SERVICE_FOUND;
    gatt_client_discover_primary_services_by_uuid16(handle_gatt_client_event, connection_handle, ORG_BLUETOOTH_SERVICE_HUMAN_INTERFACE_DEVICE);
  }
}

  l2cap_init();

  // setup SM: Display only
  sm_init();
  sm_set_io_capabilities(IO_CAPABILITY_DISPLAY_ONLY);
  sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION | SM_AUTHREQ_BONDING);

  //
  gatt_client_init();

  // register for events from HCI
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for events from Security Manager
  sm_event_callback_registration.callback = &sm_packet_handler;
  sm_add_event_handler(&sm_event_callback_registration);

Dual Mode - SPP and LE Counter

Source Code: spp_and_le_counter.c

The SPP and LE Counter example combines the Bluetooth Classic SPP Counter and the Bluetooth LE Counter into a single application.

In this Section, we only point out the differences to the individual examples and how the stack is configured.

Note: To test, please run the example, and then:

Advertisements

The Flags attribute in the Advertisement Data indicates if a device is dual-mode or le-only.

const uint8_t adv_data[] = {
  // Flags general discoverable
  0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x02,
  // Name
  0x0b, BLUETOOTH_DATA_TYPE_COMPLETE_LOCAL_NAME, 'L', 'E', ' ', 'C', 'o', 'u', 'n', 't', 'e', 'r', 
  // Incomplete List of 16-bit Service Class UUIDs -- FF10 - only valid for testing!
  0x03, BLUETOOTH_DATA_TYPE_INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS, 0x10, 0xff,
};

Packet Handler

The packet handler of the combined example is just the combination of the individual packet handlers.

Heartbeat Handler

Similar to the packet handler, the heartbeat handler is the combination of the individual ones. After updating the counter, it requests an ATT_EVENT_CAN_SEND_NOW and/or RFCOMM_EVENT_CAN_SEND_NOW

static void heartbeat_handler(struct btstack_timer_source *ts){

  if (rfcomm_channel_id || le_notification_enabled) {
    beat();
  }

  if (rfcomm_channel_id){
    rfcomm_request_can_send_now_event(rfcomm_channel_id);
  }

  if (le_notification_enabled) {
    186">att_server_request_can_send_now_event(att_con_handle);
  }

  btstack_run_loop_set_timer(ts, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(ts);
}

Main Application Setup

As with the packet and the heartbeat handlers, the combined app setup contains the code from the individual example setups.

int btstack_main(void);
164">int btstack_main(void)
{
  l2cap_init();

  rfcomm_init();
  rfcomm_register_service(packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff);

  // init SDP, create record for SPP and register with SDP
  sdp_init();
  memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
  spp_create_sdp_record(spp_service_buffer, sdp_create_service_record_handle(), RFCOMM_SERVER_CHANNEL, "SPP Counter");
  btstack_assert(de_get_len( spp_service_buffer) <= sizeof(spp_service_buffer));
  sdp_register_service(spp_service_buffer);

#ifdef ENABLE_GATT_OVER_CLASSIC
  // init SDP, create record for GATT and register with SDP
  memset(gatt_service_buffer, 0, sizeof(gatt_service_buffer));
  gatt_create_sdp_record(gatt_service_buffer, 0x10001, ATT_SERVICE_GATT_SERVICE_START_HANDLE, ATT_SERVICE_GATT_SERVICE_END_HANDLE);
  sdp_register_service(gatt_service_buffer);
  pr164">intf("SDP service record size: %u\n", de_get_len(gatt_service_buffer));
#endif

  gap_set_local_name("SPP and LE Counter 00:00:00:00:00:00");
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_discoverable_control(1);

  // setup SM: Display only
  sm_init();

  // setup ATT server
  att_server_init(profile_data, att_read_callback, att_write_callback);

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for ATT events
  1">att_server_register_packet_handler(packet_handler);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // set one-shot timer
  heartbeat.process = &heartbeat_handler;
  btstack_run_loop_set_timer(&heartbeat, HEARTBEAT_PERIOD_MS);
  btstack_run_loop_add_timer(&heartbeat);

  // beat once
  beat();

  // turn on!
    hci_power_control(HCI_POWER_ON);

  return 0;
}

Performance - Stream Data over GATT (Server)

Source Code: gatt_streamer_server.c

All newer operating systems provide GATT Client functionality. This example shows how to get a maximal throughput via BLE:

In theory, we should also update the connection parameters, but we already get a connection interval of 30 ms and there's no public way to use a shorter interval with iOS (if we're not implementing an HID device).

Note: To start the streaming, run the example. On remote device use some GATT Explorer, e.g. LightBlue, BLExplr to enable notifications.

Main Application Setup

Listing here shows main application code. It initializes L2CAP, the Security Manager, and configures the ATT Server with the pre-compiled ATT Database generated from $le_streamer.gatt$. Finally, it configures the advertisements and boots the Bluetooth stack.

static void le_streamer_setup(void){

  l2cap_init();

  // setup SM: Display only
  sm_init();

#ifdef ENABLE_GATT_OVER_CLASSIC
  // init SDP, create record for GATT and register with SDP
  sdp_init();
  memset(gatt_service_buffer, 0, sizeof(gatt_service_buffer));
  gatt_create_sdp_record(gatt_service_buffer, 0x10001, ATT_SERVICE_GATT_SERVICE_START_HANDLE, ATT_SERVICE_GATT_SERVICE_END_HANDLE);
  sdp_register_service(gatt_service_buffer);
  pr164">intf("SDP service record size: %u\n", de_get_len(gatt_service_buffer));

  // configure Classic GAP
  gap_set_local_name("GATT Streamer BR/EDR 00:00:00:00:00:00");
  gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
  gap_discoverable_control(1);
#endif

  // setup ATT server
  att_server_init(profile_data, NULL, att_write_callback);

  // register for HCI events
  hci_event_callback_registration.callback = &hci_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // register for ATT events
  1">att_server_register_packet_handler(att_packet_handler);

  // setup advertisements
  144">u164">int16_t adv_164">int_min = 0x0030;
  144">u164">int16_t adv_164">int_max = 0x0030;
  u164">int8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_164">int_min, adv_164">int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (u164">int8_t*) adv_data);
  gap_advertisements_enable(1);

  // init client state
  init_connections();
}

Track throughput

We calculate the throughput by setting a start time and measuring the amount of data sent. After a configurable REPORT_INTERVAL_MS, we print the throughput in kB/s and reset the counter and start time.

static void test_reset(le_streamer_connection_t * context){
  context->test_data_start = btstack_run_loop_get_time_ms();
  context->test_data_sent = 0;
}

static void test_track_sent(le_streamer_connection_t * context, 164">int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  u164">int32_t now = btstack_run_loop_get_time_ms();
  u164">int32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // pr164">int speed
  164">int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  pr164">intf("%c: %"PRIu32" bytes sent-> %u.%03u kB/s\n", context->name, context->test_data_sent, bytes_per_second / 1000, bytes_per_second % 1000);

  // restart
  context->test_data_start = now;
  context->test_data_sent  = 0;
}

HCI Packet Handler

The packet handler is used track incoming connections and to stop notifications on disconnect It is also a good place to request the connection parameter update as indicated in the commented code block.

static void hci_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;

  144">u164">int16_t conn_164">interval;
  hci_con_handle_t con_handle;
  static const char * const phy_names[] = {
    "1 M", "2 M", "Codec"
  };

  switch (hci_event_packet_get_type(packet)) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING) {
        pr164">intf("To start the streaming, please run the le_streamer_client example on other device, or use some GATT Explorer, e.g. LightBlue, BLExplr.\n");
      } 
      break;
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      con_handle = hci_event_disconnection_complete_get_connection_handle(packet);
      pr164">intf("- LE Connection 0x%04x: disconnect, reason %02x\n", con_handle, hci_event_disconnection_complete_get_reason(packet));          
      break;
    case HCI_EVENT_META_GAP:
      switch (hci_event_gap_meta_get_subevent_code(packet)) {
        case GAP_SUBEVENT_LE_CONNECTION_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = gap_subevent_le_connection_complete_get_connection_handle(packet);
          conn_164">interval = gap_subevent_le_connection_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connected - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), gap_subevent_le_connection_complete_get_conn_latency(packet));

          // request min con 164">interval 15 ms for iOS 11+
          pr164">intf("- LE Connection 0x%04x: request 15 ms connection 164">interval\n", con_handle);
          gap_request_connection_parameter_update(con_handle, 12, 12, 4, 0x0048);
          break;
        default:
          break;
      }
      break;
    case HCI_EVENT_LE_META:
      switch (hci_event_le_meta_get_subevent_code(packet)) {
        case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
          // pr164">int connection parameters (without using float operations)
          con_handle  = hci_subevent_le_connection_update_complete_get_connection_handle(packet);
          conn_164">interval = hci_subevent_le_connection_update_complete_get_conn_164">interval(packet);
          pr164">intf("- LE Connection 0x%04x: connection update - connection 164">interval %u.%02u ms, latency %u\n", con_handle, conn_164">interval * 125 / 100,
            25 * (conn_164">interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));
          break;
        case HCI_SUBEVENT_LE_DATA_LENGTH_CHANGE:
          con_handle = hci_subevent_le_data_length_change_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: data length change - max %u bytes per packet\n", con_handle,
               hci_subevent_le_data_length_change_get_max_tx_octets(packet));
          break;
        case HCI_SUBEVENT_LE_PHY_UPDATE_COMPLETE:
          con_handle = hci_subevent_le_phy_update_complete_get_connection_handle(packet);
          pr164">intf("- LE Connection 0x%04x: PHY update - using LE %s PHY now\n", con_handle,
               phy_names[hci_subevent_le_phy_update_complete_get_tx_phy(packet)]);
          break;
        default:
          break;
      }
      break;

    default:
      break;
  }
}

ATT Packet Handler

The packet handler is used to track the ATT MTU Exchange and trigger ATT send

static void att_packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  164">int mtu;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        case ATT_EVENT_CONNECTED:
          // setup new 
          context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
          if (!context) break;
          context->counter = 'A';
          context->connection_handle = att_event_connected_get_handle(packet);
          context->test_data_len = btstack_min(103">att_server_get_mtu(context->connection_handle) - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT connected, handle 0x%04x, test data len %u\n", context->name, context->connection_handle, context->test_data_len);
          break;
        case ATT_EVENT_MTU_EXCHANGE_COMPLETE:
          mtu = att_event_mtu_exchange_complete_get_MTU(packet) - 3;
          context = connection_for_conn_handle(att_event_mtu_exchange_complete_get_handle(packet));
          if (!context) break;
          context->test_data_len = btstack_min(mtu - 3, sizeof(context->test_data));
          pr164">intf("%c: ATT MTU = %u => use test data of len %u\n", context->name, mtu, context->test_data_len);
          break;
        case ATT_EVENT_CAN_SEND_NOW:
          streamer();
          break;
        case ATT_EVENT_DISCONNECTED:
          context = connection_for_conn_handle(att_event_disconnected_get_handle(packet));
          if (!context) break;
          // free connection
          pr164">intf("%c: ATT disconnected, handle 0x%04x\n", context->name, context->connection_handle);          
          context->le_notification_enabled = 0;
          context->connection_handle = HCI_CON_HANDLE_INVALID;
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}

Streamer

The streamer function checks if notifications are enabled and if a notification can be sent now. It creates some test data - a single letter that gets increased every time - and tracks the data sent.

static void streamer(void){

  // find next active streaming connection
  164">int old_connection_index = connection_index;
  while (1){
    // active found?
    if ((le_streamer_connections[connection_index].connection_handle != HCI_CON_HANDLE_INVALID) &&
      (le_streamer_connections[connection_index].le_notification_enabled)) break;

    // check next
    next_connection_index();

    // none found
    if (connection_index == old_connection_index) return;
  }

  le_streamer_connection_t * context = &le_streamer_connections[connection_index];

  // create test data
  context->counter++;
  if (context->counter > 'Z') context->counter = 'A';
  memset(context->test_data, context->counter, context->test_data_len);

  // send
  131">att_server_notify(context->connection_handle, context->value_handle, (u164">int8_t*) context->test_data, context->test_data_len);

  // track
  test_track_sent(context, context->test_data_len);

  // request next send event
  186">att_server_request_can_send_now_event(context->connection_handle);

  // check next
  next_connection_index();
}

ATT Write

The only valid ATT write in this example is to the Client Characteristic Configuration, which configures notification and indication. If the ATT handle matches the client configuration handle, the new configuration value is stored. If notifications get enabled, an ATT_EVENT_CAN_SEND_NOW is requested. See Listing here.

static int att_write_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t transaction_mode, uint16_t offset, uint8_t *buffer, uint16_t buffer_size){
  UNUSED(offset);

  // pr164">intf("att_write_callback att_handle 0x%04x, transaction mode %u\n", att_handle, transaction_mode);
  if (transaction_mode != ATT_TRANSACTION_MODE_NONE) return 0;
  le_streamer_connection_t * context = connection_for_conn_handle(con_handle);
  switch(att_handle){
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
      context->le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
      pr164">intf("%c: Notifications enabled %u\n", context->name, context->le_notification_enabled); 
      if (context->le_notification_enabled){
        switch (att_handle){
          case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE:
            context->value_handle = ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE;
            break;
          default:
            break;
        }
        186">att_server_request_can_send_now_event(context->connection_handle);
      }
      test_reset(context);
      break;
    case ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
    case ATT_CHARACTERISTIC_0000FF12_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE:
      test_track_sent(context, buffer_size);
      break;
    default:
      pr164">intf("Write to 0x%04x, len %u\n", att_handle, buffer_size);
      break;
  }
  return 0;
}

SDP Client - Query Remote SDP Records

Source Code: sdp_general_query.c

The example shows how the SDP Client is used to get a list of service records on a remote device.

SDP Client Setup

SDP is based on L2CAP. To receive SDP query events you must register a callback, i.e. query handler, with the SPD parser, as shown in Listing here. Via this handler, the SDP client will receive the following events:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void handle_sdp_client_query_result(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void sdp_general_query_init(void){
  // init L2CAP
  l2cap_init();

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

SDP Client Query

To trigger an SDP query to get the a list of service records on a remote device, you need to call sdp_client_query_uuid16() with the remote address and the UUID of the public browse group, as shown in Listing here. In this example we used fixed address of the remote device shown in Listing here. Please update it with the address of a device in your vicinity, e.g., one reported by the GAP Inquiry example in the previous section.

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;
  u164">int8_t event = hci_event_packet_get_type(packet);

  switch (event) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started 
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
        pr164">intf("Connecting to %s\n", bd_addr_to_str(remote_addr));
        sdp_client_query_uuid16(&handle_sdp_client_query_result, remote_addr, BLUETOOTH_PROTOCOL_L2CAP);
      }
      break;
    default:
      break;
  }
}

Handling SDP Client Query Results

The SDP Client returns the results of the query in chunks. Each result packet contains the record ID, the Attribute ID, and a chunk of the Attribute value. In this example, we append new chunks for the same Attribute ID in a large buffer, see Listing here.

To save memory, it's also possible to process these chunks directly by a custom stream parser, similar to the way XML files are parsed by a SAX parser. Have a look at src/sdp_client_rfcomm.c which retrieves the RFCOMM channel number and the service name.

static void handle_sdp_client_query_result(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(packet_type);
  UNUSED(channel);
  UNUSED(size);

  switch (hci_event_packet_get_type(packet)){
    case SDP_EVENT_QUERY_ATTRIBUTE_VALUE:
      // handle new record
      if (sdp_event_query_attribute_byte_get_record_id(packet) != record_id){
        record_id = sdp_event_query_attribute_byte_get_record_id(packet);
        pr164">intf("\n---\nRecord nr. %u\n", record_id);
      }

      assertBuffer(sdp_event_query_attribute_byte_get_attribute_length(packet));

      attribute_value[sdp_event_query_attribute_byte_get_data_offset(packet)] = sdp_event_query_attribute_byte_get_data(packet);
      if ((144">u164">int16_t)(sdp_event_query_attribute_byte_get_data_offset(packet)+1) == sdp_event_query_attribute_byte_get_attribute_length(packet)){
         pr164">intf("Attribute 0x%04x: ", sdp_event_query_attribute_byte_get_attribute_id(packet));
         de_dump_data_element(attribute_value);
      }
      break;
    case SDP_EVENT_QUERY_COMPLETE:
      if (sdp_event_query_complete_get_status(packet)){
        pr164">intf("SDP query failed 0x%02x\n", sdp_event_query_complete_get_status(packet));
        break;
      } 
      pr164">intf("SDP query done.\n");
      break;
    default:
      break;
  }
}

SDP Client - Query RFCOMM SDP record

Source Code: sdp_rfcomm_query.c

The example shows how the SDP Client is used to get all RFCOMM service records from a remote device. It extracts the remote RFCOMM Server Channel, which are needed to connect to a remote RFCOMM service.

SDP Client - Query BNEP SDP record

Source Code: sdp_bnep_query.c

The example shows how the SDP Client is used to get all BNEP service records from a remote device. It extracts the remote BNEP PAN protocol UUID and the L2CAP PSM, which are needed to connect to a remote BNEP service.

SDP Client Setup

As with the previous example, you must register a callback, i.e. query handler, with the SPD parser, as shown in Listing here. Via this handler, the SDP client will receive events:

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void handle_sdp_client_query_result(u164">int8_t packet_type, 144">u164">int16_t channel, u164">int8_t *packet, 144">u164">int16_t size);

static void sdp_bnep_qeury_init(void){
  // init L2CAP
  l2cap_init();

  // register for HCI events
  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);
}

SDP Client Query

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);

  if (packet_type != HCI_EVENT_PACKET) return;
  u164">int8_t event = hci_event_packet_get_type(packet);

  switch (event) {
    case BTSTACK_EVENT_STATE:
      // BTstack activated, get started 
      if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
        pr164">intf("Start SDP BNEP query.\n");
        sdp_client_query_uuid16(&handle_sdp_client_query_result, remote, BLUETOOTH_PROTOCOL_BNEP);
      }
      break;
    default:
      break;
  }
}

Handling SDP Client Query Result

The SDP Client returns the result of the query in chunks. Each result packet contains the record ID, the Attribute ID, and a chunk of the Attribute value, see Listing here. Here, we show how to parse the Service Class ID List and Protocol Descriptor List, as they contain the BNEP Protocol UUID and L2CAP PSM respectively.

static void handle_sdp_client_query_result(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(packet_type);
  UNUSED(channel);
  UNUSED(size);

...

        switch(sdp_event_query_attribute_byte_get_attribute_id(packet)){
          // 0x0001 "Service Class ID List"
          case BLUETOOTH_ATTRIBUTE_SERVICE_CLASS_ID_LIST:
            if (de_get_element_type(attribute_value) != DE_DES) break;
            for (des_iterator_init(&des_list_it, attribute_value); des_iterator_has_more(&des_list_it); des_iterator_next(&des_list_it)){
              u164">int8_t * element = des_iterator_get_element(&des_list_it);
              if (de_get_element_type(element) != DE_UUID) continue;
              u164">int32_t uuid = de_get_uuid32(element);
              switch (uuid){
                case BLUETOOTH_SERVICE_CLASS_PANU:
                case BLUETOOTH_SERVICE_CLASS_NAP:
                case BLUETOOTH_SERVICE_CLASS_GN:
                  pr164">intf(" ** Attribute 0x%04x: BNEP PAN protocol UUID: %04x\n", sdp_event_query_attribute_byte_get_attribute_id(packet), (164">int) uuid);
                  break;
                default:
                  break;
              }
            }
            break;
...
          case BLUETOOTH_ATTRIBUTE_PROTOCOL_DESCRIPTOR_LIST:{
              pr164">intf(" ** Attribute 0x%04x: ", sdp_event_query_attribute_byte_get_attribute_id(packet));

              144">u164">int16_t l2cap_psm = 0;
              144">u164">int16_t bnep_version = 0;
              for (des_iterator_init(&des_list_it, attribute_value); des_iterator_has_more(&des_list_it); des_iterator_next(&des_list_it)){
                if (des_iterator_get_type(&des_list_it) != DE_DES) continue;
                u164">int8_t * des_element = des_iterator_get_element(&des_list_it);
                des_iterator_init(&prot_it, des_element);
                u164">int8_t * element = des_iterator_get_element(&prot_it);

                if (de_get_element_type(element) != DE_UUID) continue;
                u164">int32_t uuid = de_get_uuid32(element);
                des_iterator_next(&prot_it);
                switch (uuid){
                  case BLUETOOTH_PROTOCOL_L2CAP:
                    if (!des_iterator_has_more(&prot_it)) continue;
                    de_element_get_u164">int16(des_iterator_get_element(&prot_it), &l2cap_psm);
                    break;
                  case BLUETOOTH_PROTOCOL_BNEP:
                    if (!des_iterator_has_more(&prot_it)) continue;
                    de_element_get_u164">int16(des_iterator_get_element(&prot_it), &bnep_version);
                    break;
                  default:
                    break;
                }
              }
              pr164">intf("l2cap_psm 0x%04x, bnep_version 0x%04x\n", l2cap_psm, bnep_version);
            }
            break;
...
}

The Service Class ID List is a Data Element Sequence (DES) of UUIDs. The BNEP PAN protocol UUID is within this list.

The Protocol Descriptor List is DES which contains one DES for each protocol. For PAN serivces, it contains a DES with the L2CAP Protocol UUID and a PSM, and another DES with the BNEP UUID and the the BNEP version.

PBAP Client - Get Contacts from Phonebook Server

Source Code: pbap_client_demo.c

Note: The Bluetooth address of the remote Phonbook server is hardcoded. Change it before running example, then use the UI to connect to it, to set and query contacts.

Testing - Enable Device Under Test (DUT) Mode for Classic

Source Code: dut_mode_classic.c

DUT mode can be used for production testing. This example just configures the Bluetooth Controller for DUT mode.

Bluetooth Logic

When BTstack is up and running, send Enable Device Under Test Mode Command and print its result.

For more details on discovering remote devices, please see Section on GAP.

Main Application Setup

Listing here shows main application code. It registers the HCI packet handler and starts the Bluetooth stack.

int btstack_main(int argc, const char * argv[]);
164">int btstack_main(164">int argc, const char * argv[]) {
  (void)argc;
  (void)argv;

  // disable Secure Simple Pairinng
  gap_ssp_set_enable(0);

  // make device connectable
  // @note: gap_connectable_control will be enabled when an L2CAP service 
  // (e.g. RFCOMM) is initialized). Therefore, it's not needed in regular applications
  gap_connectable_control(1);

  // make device discoverable
  gap_discoverable_control(1);

  hci_event_callback_registration.callback = &packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  // turn on!
  hci_power_control(HCI_POWER_ON);

  return 0;
}