In this section, we will describe a number of examples from the example folder. To allow code-reuse with different platforms as well as with new ports, the low-level initialization of BTstack and the hardware configuration has been extracted to the various platforms/PLATFORM/main.c files. The examples only contain the platform-independent Bluetooth logic. But let’s have a look at the common init code.

Listing below shows a minimal platform setup for an embedded system with a Bluetooth chipset connected via UART.

int main(){
  // ... hardware init: watchdoch, IOs, timers, etc...

  // setup BTstack memory pools
  btstack_memory_init();

  // select embedded run loop
  btstack_run_loop_init(btstack_run_loop_embedded_get_instance());

  // use logger: format HCI_DUMP_PACKETLOGGER, HCI_DUMP_BLUEZ or HCI_DUMP_STDOUT
  hci_dump_open(NULL, HCI_DUMP_STDOUT);

  // init HCI
  hci_transport_t     * transport = hci_transport_h4_instance();
  hci_init(transport, NULL);

  // setup example    
  btstack_main(argc, argv);

  // go
  btstack_run_loop_execute();    
}

First, BTstack’s memory pools are setup up. Then, the standard run loop implementation for embedded systems is selected.

The call to hci_dump_open configures BTstack to output all Bluetooth packets and it’s own debug and error message via printf. The Python script tools/create_packet_log.py can be used to convert the console output into a Bluetooth PacketLogger format that can be opened by the OS X PacketLogger tool as well as by Wireshark for further inspection. When asking for help, please always include a log created with HCI dump.

The hci_init function sets up HCI to use the HCI H4 Transport implementation. It doesn’t provide a special transport configuration nor a special implementation for a particular Bluetooth chipset. It makes use of the remote_device_db_memory implementation that allows for re-connects without a new pairing but doesn’t persist the bonding information.

Finally, it calls btstack_main() of the actual example before executing the run loop.

led_counter: Hello World: blinking LED without Bluetooth

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
  char lineBuffer[30];
  sprintf(lineBuffer, "BTstack counter %04u\n\r", ++counter);
  puts(lineBuffer);

  // 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[]);
int btstack_main(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);

  printf("Running...\n\r");
  return 0;
}

gap_inquiry: GAP Inquiry Example

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:

  • GAP Inquiry result event: BTstack provides a unified inquiry result that contain Class of Device (CoD), page scan mode, clock offset. RSSI and name (from EIR) are optional.
  • Inquiry complete event: the remote name is requested for devices without a fetched name. The state of a remote name can be one of the following: REMOTE_NAME_REQUEST, REMOTE_NAME_INQUIRED, or REMOTE_NAME_FETCHED.
  • Remote name request complete event: the remote name is stored in the table and the state is updated to REMOTE_NAME_FETCHED. The query of remote names is continued.

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[]);
int btstack_main(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;
}

sdp_general_query: Dump remote SDP Records

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:

  • SDP_EVENT_QUERY_ATTRIBUTE_VALUE containing the results of the query in chunks,
  • SDP_EVENT_QUERY_COMPLETE indicating the end of the query and the status

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

static void sdp_client_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 bd_addr_t remote = {0x04,0x0C,0xCE,0xE4,0x85,0xD3};

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;
  uint8_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){
        sdp_client_query_uuid16(&handle_sdp_client_query_result, remote, BLUETOOTH_ATTRIBUTE_PUBLIC_BROWSE_ROOT);
      }
      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 (packet[0]){
    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);
        printf("\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 ((uint16_t)(sdp_event_query_attribute_byte_get_data_offset(packet)+1) == sdp_event_query_attribute_byte_get_attribute_length(packet)){
         printf("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)){
        printf("SDP query failed 0x%02x\n", sdp_event_query_complete_get_status(packet));
        break;
      } 
      printf("SDP query done.\n");
      break;
  }
}

sdp_bnep_query: Dump remote BNEP PAN protocol UUID and L2CAP PSM

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:

  • SDP_EVENT_QUERY_ATTRIBUTE_VALUE containing the results of the query in chunks,
  • SDP_EVENT_QUERY_COMPLETE reporting the status and the end of the query.

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

static void sdp_client_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;
  uint8_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){
        printf("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)){
              uint8_t * element = des_iterator_get_element(&des_list_it);
              if (de_get_element_type(element) != DE_UUID) continue;
              uint32_t uuid = de_get_uuid32(element);
              switch (uuid){
                case BLUETOOTH_SERVICE_CLASS_PANU:
                case BLUETOOTH_SERVICE_CLASS_NAP:
                case BLUETOOTH_SERVICE_CLASS_GN:
                  printf(" ** Attribute 0x%04x: BNEP PAN protocol UUID: %04x\n", sdp_event_query_attribute_byte_get_attribute_id(packet), (int) uuid);
                  break;
                default:
                  break;
              }
            }
            break;
...
          case BLUETOOTH_ATTRIBUTE_PROTOCOL_DESCRIPTOR_LIST:{
              printf(" ** Attribute 0x%04x: ", sdp_event_query_attribute_byte_get_attribute_id(packet));

              uint16_t l2cap_psm = 0;
              uint16_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;
                uint8_t * des_element = des_iterator_get_element(&des_list_it);
                des_iterator_init(&prot_it, des_element);
                uint8_t * element = des_iterator_get_element(&prot_it);

                if (de_get_element_type(element) != DE_UUID) continue;
                uint32_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_uint16(des_iterator_get_element(&prot_it), &l2cap_psm);
                    break;
                  case BLUETOOTH_PROTOCOL_BNEP:
                    if (!des_iterator_has_more(&prot_it)) continue;
                    de_element_get_uint16(des_iterator_get_element(&prot_it), &bnep_version);
                    break;
                  default:
                    break;
                }
              }
              printf("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.

spp_counter: SPP Server - Heartbeat Counter over RFCOMM

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

  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, 0x10001, RFCOMM_SERVER_CHANNEL, "SPP Counter");
  sdp_register_service(spp_service_buffer);
  printf("SDP service record size: %u\n", de_get_len(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 int counter = 0;

  if (rfcomm_channel_id){
    sprintf(lineBuffer, "BTstack counter %04u\n", ++counter);
    printf("%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:

  • BTSTACK_EVENT_STATE,
  • HCI_EVENT_PIN_CODE_REQUEST (Standard pairing) or
  • HCI_EVENT_USER_CONFIRMATION_REQUEST (Secure Simple Pairing),
  • RFCOMM_EVENT_INCOMING_CONNECTION,
  • RFCOMM_EVENT_CHANNEL_OPENED,
  • RFCOMM_EVENT_CHANNEL_CLOSED

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
          printf("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
          printf("SSP User Confirmation Request with numeric value '%06"PRIu32"'\n", little_endian_read_32(packet, 8));
          printf("SSP User Confirmation Auto accept\n");
          break;

        case RFCOMM_EVENT_INCOMING_CONNECTION:
          // data: event (8), len(8), address(48), channel (8), rfcomm_cid (16)
          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);
          printf("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:
          // data: event(8), len(8), status (8), address (48), server channel(8), rfcomm_cid(16), max frame size(16)
          if (rfcomm_event_channel_opened_get_status(packet)) {
            printf("RFCOMM channel open failed, status %u\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);
            printf("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, (uint8_t*) lineBuffer, strlen(lineBuffer));  
          break;

...
}

spp_flowcontrol: SPP Server - Flow Control

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, 0x10001, 1, "SPP Counter");
  sdp_register_service(spp_service_buffer);
  printf("SDP service buffer size: %u\n\r", (uint16_t) de_get_len(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 (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
...
    case RFCOMM_DATA_PACKET:
      for (i=0;i<size;i++){
        putchar(packet[i]);
      };
      putchar('\n');
      rfcomm_send_credit = 1;
      break;
...
}

panu_demo: PANU Demo

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 Mac.

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(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static void network_send_packet_callback(const uint8_t * packet, uint16_t size);

static void panu_setup(void){

  // Initialize L2CAP 
  l2cap_init();

  // Initialise BNEP
  bnep_init();

  // Minimum L2CAP MTU for bnep is 1691 bytes
  bnep_register_service(packet_handler, BLUETOOTH_SERVICE_CLASS_PANU, 1691);

  // Initialize network 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) {      
        case BTSTACK_EVENT_STATE:
          if (btstack_event_state_get_state(packet) == HCI_STATE_WORKING){
            printf("Start SDP BNEP query for remote PAN Network Access Point (NAP).\n");
            sdp_client_query_uuid16(&handle_sdp_client_query_result, remote_addr, BLUETOOTH_SERVICE_CLASS_NAP);
          }
          break;

...

                case BNEP_EVENT_CHANNEL_OPENED:
          if (bnep_event_channel_opened_get_status(packet)) {
            printf("BNEP channel open failed, status %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);
            memcpy(&event_addr, &packet[11], sizeof(bd_addr_t));
            printf("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);
            printf("Network Interface %s activated\n", btstack_network_get_name());
          }
                    break;

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

        case BNEP_EVENT_CHANNEL_CLOSED:
          printf("BNEP channel closed\n");
          btstack_network_down();
          break;

        case BNEP_EVENT_CAN_SEND_NOW:
          if (network_buffer_len > 0) {
            bnep_send(bnep_cid, (uint8_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 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);
}

hsp_hs_demo: HSP Headset Demo

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:

  • Ubuntu 14 64-bit, CC2564B connected via FTDI USB-2-UART adapter, 921600 baud
  • Ubuntu 14 64-bit, CSR USB dongle
  • OS X 10.11, CSR USB dongle

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:

  • The HCI SCO packet handler receives audio data.
  • The HSP HS packet handler is used to trigger sending of audio data and commands to the AG. It also receives the AG's answers.

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

  sco_demo_init();
  sco_demo_set_codec(HFP_CODEC_CVSD);

  l2cap_init();

  sdp_init();
  memset(hsp_service_buffer, 0, sizeof(hsp_service_buffer));
  hsp_hs_create_sdp_record(hsp_service_buffer, 0x10001, rfcomm_channel_nr, hsp_hs_service_name, 0);
  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;
}

hsp_ag_demo: HSP Audio Gateway Demo

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:

  • Ubuntu 14 64-bit, CC2564B connected via FTDI USB-2-UART adapter, 921600 baud
  • Ubuntu 14 64-bit, CSR USB dongle
  • OS X 10.11, CSR USB dongle

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:

  • The HCI SCO packet handler receives audio data.
  • The HSP AG packet handler is used to trigger sending of audio data and commands to the HS. It also receives the AG's answers.

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

  sco_demo_init();
  sco_demo_set_codec(HFP_CODEC_CVSD);

  l2cap_init();

  sdp_init();

  memset((uint8_t *)hsp_service_buffer, 0, sizeof(hsp_service_buffer));
  hsp_ag_create_sdp_record(hsp_service_buffer, 0x10001, rfcomm_channel_nr, hsp_ag_service_name);
  printf("SDP service record size: %u\n", de_get_len(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;
}

hfp_hs_demo: HFP Hands-Free (HF) Demo

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[]);
int btstack_main(int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  sco_demo_init();

  gap_discoverable_control(1);
  gap_set_class_of_device(0x200408);   
  gap_set_local_name("HFP HF Demo 00:00:00:00:00:00");

  // init L2CAP
  l2cap_init();

  uint16_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_REMOTE_VOLUME_CONTROL);
  int wide_band_speech = 1;

  rfcomm_init();
  hfp_hf_init(rfcomm_channel_nr);
  hfp_hf_init_supported_features(hf_supported_features);
  hfp_hf_init_hf_indicators(sizeof(indicators)/sizeof(uint16_t), indicators);
  hfp_hf_init_codecs(sizeof(codecs), codecs);

  sdp_init();  
  memset(hfp_service_buffer, 0, sizeof(hfp_service_buffer));
  hfp_hf_create_sdp_record(hfp_service_buffer, 0x10001, rfcomm_channel_nr, hfp_hf_service_name, hf_supported_features, wide_band_speech);
  printf("SDP service record size: %u\n", de_get_len(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);
  hci_register_sco_packet_handler(&packet_handler);

  // register for HFP events
  hfp_hf_register_packet_handler(packet_handler);

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

hfp_ag_demo: HFP Audio Gateway (AG) Demo

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[]);
int btstack_main(int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  sco_demo_init();

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

  // L2CAP
  l2cap_init();

  uint16_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_THREE_WAY_CALLING);
  int wide_band_speech = 1;

  // 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( hfp_service_buffer, 0x10001, rfcomm_channel_nr, hfp_ag_service_name, 0, supported_features, wide_band_speech);
  printf("SDP service record size: %u\n", de_get_len( 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 humand 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;
}

gap_le_advertisements: GAP LE Advertisements Dumper

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 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;
  uint8_t uuid_128[16];
  for (ad_iterator_init(&context, adv_size, (uint8_t *)adv_data) ; ad_iterator_has_more(&context) ; ad_iterator_next(&context)){
    uint8_t data_type  = ad_iterator_get_data_type(&context);
    uint8_t size     = ad_iterator_get_data_len(&context);
    const uint8_t * data = ad_iterator_get_data(&context);

    if (data_type > 0 && data_type < 0x1B){
      printf("  %s: ", ad_types[data_type]);
    } 
    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)){
            printf("%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){
          printf("%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){
          printf("%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);
        printf("%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++){
          printf("%c", (char)(data[i]));
        }
        break;
      case BLUETOOTH_DATA_TYPE_TX_POWER_LEVEL:
        printf("%d dBm", *(int8_t*)data);
        break;
      case BLUETOOTH_DATA_TYPE_SLAVE_CONNECTION_INTERVAL_RANGE:
        printf("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:
        printf_hexdump(data, size);
        break;
      case BLUETOOTH_DATA_TYPE_PUBLIC_TARGET_ADDRESS:
      case BLUETOOTH_DATA_TYPE_RANDOM_TARGET_ADDRESS:
        reverse_bd_addr(data, address);
        printf("%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
        printf("%02X", little_endian_read_16(data, 0) );
        break;
      case BLUETOOTH_DATA_TYPE_ADVERTISING_INTERVAL:
        printf("%u ms", little_endian_read_16(data, 0) * 5/8 );
        break;
      case BLUETOOTH_DATA_TYPE_3D_INFORMATION_DATA:
        printf_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:
        printf("Advertising Data Type 0x%2x not handled yet", data_type); 
        break;
    }    
    printf("\n");
  }
  printf("\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;

  switch (hci_event_packet_get_type(packet)) {
    case GAP_EVENT_ADVERTISING_REPORT:{
      bd_addr_t address;
      gap_event_advertising_report_get_address(packet, address);
      uint8_t event_type = gap_event_advertising_report_get_advertising_event_type(packet);
      uint8_t address_type = gap_event_advertising_report_get_address_type(packet);
      int8_t rssi = gap_event_advertising_report_get_rssi(packet);
      uint8_t length = gap_event_advertising_report_get_data_length(packet);
      const uint8_t * data = gap_event_advertising_report_get_data(packet);

      printf("Advertisement event: evt-type %u, addr-type %u, addr %s, rssi %d, data[%u] ", event_type,
         address_type, bd_addr_to_str(address), rssi, length);
      printf_hexdump(data, length);
      dump_advertisement_data(data, length);
      break;
    }
    default:
      break;
  }
}

gatt_browser: GATT Client - Discovering primary services and their characteristics

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(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_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(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_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;

  uint8_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){
        printf("Trying to connect to %s\n", bd_addr_to_str(cmdline_addr));
        gap_connect(cmdline_addr, 0);
        break;
      }
      printf("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_LE_META:
      // wait for connection complete
      if (hci_event_le_meta_get_subevent_code(packet) !=  HCI_SUBEVENT_LE_CONNECTION_COMPLETE) break;
      connection_handler = hci_subevent_le_connection_complete_get_connection_handle(packet);
      // query primary services
      gatt_client_discover_primary_services(handle_gatt_client_event, connection_handler);
      break;
    case HCI_EVENT_DISCONNECTION_COMPLETE:
      printf("\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 int search_services = 1;

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:
      if (search_services){
        // GATT_EVENT_QUERY_COMPLETE of search services 
        service_index = 0;
        printf("\nGATT browser - CHARACTERISTIC for SERVICE %s\n", uuid128_to_str(service.uuid128));
        search_services = 0;
        gatt_client_discover_characteristics_for_service(handle_gatt_client_event, connection_handler, &services[service_index]);
      } else {
        // GATT_EVENT_QUERY_COMPLETE of search characteristics
        if (service_index < service_count) {
          service = services[service_index++];
          printf("\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_handler, &service);
          break;
        }
        service_index = 0;
        gap_disconnect(connection_handler); 
      }
      break;
    default:
      break;
  }
}

le_counter: LE Peripheral - Heartbeat Counter over GATT

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 uint8_t battery = 100;

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
static uint16_t att_read_callback(hci_con_handle_t con_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_t buffer_size);
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);
static void  heartbeat_handler(struct btstack_timer_source *ts);
static void beat(void);

const uint8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 
  // 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 uint8_t adv_data_len = sizeof(adv_data);

static void le_counter_setup(void){

  l2cap_init();

  // setup le device db
  le_device_db_init();

  // setup SM: Display only
  sm_init();

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

  // setup battery service
  battery_service_server_init(battery);

  // setup advertisements
  uint16_t adv_int_min = 0x0030;
  uint16_t adv_int_max = 0x0030;
  uint8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (uint8_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
  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 int  counter_string_len;

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

static void heartbeat_handler(struct btstack_timer_source *ts){
  if (le_notification_enabled) {
    beat();
    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:

  • stop the counter after a disconnect
  • send a notification when the requested ATT_EVENT_CAN_SEND_NOW is received

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:
          le_notification_enabled = 0;
          break;
        case ATT_EVENT_CAN_SEND_NOW:
          att_server_notify(con_handle, ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_VALUE_HANDLE, (uint8_t*) counter_string, counter_string_len);
          break;
      }
      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 uint16_t att_read_callback(hci_con_handle_t connection_handle, uint16_t att_handle, uint16_t offset, uint8_t * buffer, uint16_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 uint8_t *)counter_string, buffer_size, offset, buffer, buffer_size);
  }
  return 0;
}

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 and used in the heartbeat handler to decide if a new value should be sent. 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){
  UNUSED(transaction_mode);
  UNUSED(offset);
  UNUSED(buffer_size);

  if (att_handle != ATT_CHARACTERISTIC_0000FF11_0000_1000_8000_00805F9B34FB_01_CLIENT_CONFIGURATION_HANDLE) return 0;
  le_notification_enabled = little_endian_read_16(buffer, 0) == GATT_CLIENT_CHARACTERISTICS_CONFIGURATION_NOTIFICATION;
  con_handle = connection_handle;
  return 0;
}

le_streamer: LE Streamer - Stream data over GATT.

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

  • send whenever possible,
  • use the max ATT MTU.

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 le device db
  le_device_db_init();

  // setup SM: Display only
  sm_init();

  // 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
  att_server_register_packet_handler(att_packet_handler);

  // setup advertisements
  uint16_t adv_int_min = 0x0030;
  uint16_t adv_int_max = 0x0030;
  uint8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (uint8_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, int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  uint32_t now = btstack_run_loop_get_time_ms();
  uint32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // print speed
  int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  printf("%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);

  uint16_t conn_interval;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      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) {
            printf("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:
          context = connection_for_conn_handle(hci_event_disconnection_complete_get_connection_handle(packet));
          if (!context) break;
          // free connection
          printf("%c: Disconnect, reason %02x\n", context->name, hci_event_disconnection_complete_get_reason(packet));          
          context->le_notification_enabled = 0;
          context->connection_handle = HCI_CON_HANDLE_INVALID;
          break;
        case HCI_EVENT_LE_META:
          switch (hci_event_le_meta_get_subevent_code(packet)) {
            case HCI_SUBEVENT_LE_CONNECTION_COMPLETE:
              // setup new 
              context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
              if (!context) break;
              context->counter = 'A';
              context->test_data_len = ATT_DEFAULT_MTU - 3;
              context->connection_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
              // print connection parameters (without using float operations)
              conn_interval = hci_subevent_le_connection_complete_get_conn_interval(packet);
              printf("%c: Connection Interval: %u.%02u ms, latency %u\n", context->name, conn_interval * 125 / 100,
                25 * (conn_interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));

              // request min con interval 15 ms for iOS 11+ 
              // gap_request_connection_parameter_update(context->connection_handle, 12, 12, 0, 0x0048);
              break;
            case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
              // print connection parameters (without using float operations)
              context = connection_for_conn_handle(hci_subevent_le_connection_update_complete_get_connection_handle(packet));
              if (!context) break;
              conn_interval = hci_subevent_le_connection_update_complete_get_conn_interval(packet);
              printf("%c: Connection Interval: %u.%02u ms, latency %u\n", context->name, conn_interval * 125 / 100,
                25 * (conn_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 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);

  int mtu;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        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));
          printf("%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;
        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
  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
  att_server_notify(context->connection_handle, context->value_handle, (uint8_t*) context->test_data, context->test_data_len);

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

  // request next send event
  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);

  // printf("att_write_callback att_handle %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;
      printf("%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;
        }
        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:
      printf("Write to 0x%04x, len %u\n", att_handle, buffer_size);
  }
  return 0;
}

spp_and_le_counter: Dual mode example

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:

  • for SPP pair from a remote device, and open the Virtual Serial Port,
  • for LE use some GATT Explorer, e.g. LightBlue, BLExplr, to enable notifications.

Advertisements

The Flags attribute in the Advertisement Data indicates if a device is in dual-mode or not. Flag 0x06 indicates LE General Discoverable, BR/EDR not supported although we're actually using BR/EDR. In the past, there have been problems with Anrdoid devices when the flag was not set. Setting it should prevent the remote implementation to try to use GATT over LE/EDR, which is not implemented by BTstack. So, setting the flag seems like the safer choice (while it's technically incorrect).

const uint8_t adv_data[] = {
  // Flags general discoverable, BR/EDR not supported
  0x02, BLUETOOTH_DATA_TYPE_FLAGS, 0x06, 
  // 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) {
    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);
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, 0x10001, RFCOMM_SERVER_CHANNEL, "SPP Counter");
  sdp_register_service(spp_service_buffer);
  printf("SDP service record size: %u\n", de_get_len(spp_service_buffer));

  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 le device db
  le_device_db_init();

  // 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
  att_server_register_packet_handler(packet_handler);

  // setup advertisements
  uint16_t adv_int_min = 0x0030;
  uint16_t adv_int_max = 0x0030;
  uint8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (uint8_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;
}

a2dp_sink_demo: Receive audio stream and control its playback.

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.

@test 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.

@test To controll the playback, tap SPACE on the console to show the available AVRCP commands.

Main Application Setup

The Listing here shows how to setup AD2P Sink and AVRCP controller services. To announce A2DP Sink and AVRCP Controller services, you need to create corresponding SDP records and register them with the SDP service. You'll also need to register several packet handlers:

  • a2dp_sink_packet_handler - handles events on stream connection status (established, released), the media codec configuration, and, the status of the stream itself (opened, paused, stopped).
  • handle_l2cap_media_data_packet - used to receive streaming data. If HAVE_PORTAUDIO or STORE_SBC_TO_WAV_FILE directives (check btstack_config.h) are used, the SBC decoder will be used to decode the SBC data into PCM frames. The resulting PCM frames are then processed in the SBC Decoder callback.
  • stdin_process callback - used to trigger AVRCP commands to the A2DP Source device, such are get now playing info, start, stop, volume control. Requires HAVE_BTSTACK_STDIN.
  • avrcp_controller_packet_handler - used to receive answers for AVRCP commands,

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_SBC_TO_WAV_FILE. The HAVE_PORTAUDIO directive indicates if the audio is played back via PortAudio. If HAVE_PORTAUDIO or STORE_SBC_TO_WAV_FILE directives 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:

  • handle_pcm_data - handles PCM audio frames. Here, they are stored a in wav file if STORE_SBC_TO_WAV_FILE is defined, and/or played using the PortAudio library if HAVE_PORTAUDIO is defined.

Handle Media Data Packet

a2dp_source_demo: Serve audio stream and handle remote playback control and queries.

Media data packets, in this case 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.

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.

@test 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. Tap SPACE on the console to show the available commands.

Main Application Setup

The Listing here shows how to setup AD2P Source and AVRCP Target services.

static void a2dp_source_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t * event, uint16_t event_size);
static void avrcp_target_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);
#ifdef HAVE_BTSTACK_STDIN
static void stdin_process(char cmd);
#endif

static int a2dp_source_and_avrcp_services_init(void){

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

  // Create stream endpoint.
  avdtp_stream_endpoint_t * local_stream_endpoint = a2dp_source_create_stream_endpoint(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_endpoint){
    printf("A2DP Source: not enough memory to create local stream endpoint\n");
    return 1;
  }
  media_tracker.local_seid = avdtp_local_seid(local_stream_endpoint);

  // Initialize AVRCP Target.
  avrcp_target_init();
  avrcp_target_register_packet_handler(&avrcp_target_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, 0x10002, 1, NULL, NULL);
  sdp_register_service(sdp_a2dp_source_service_buffer);

  // Create AVRCP target service record and register it with SDP.
  memset(sdp_avrcp_target_service_buffer, 0, sizeof(sdp_avrcp_target_service_buffer));
  avrcp_target_create_sdp_record(sdp_avrcp_target_service_buffer, 0x10001, AVRCP_BROWSING_ENABLED, 1, NULL, NULL);
  sdp_register_service(sdp_avrcp_target_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 = &a2dp_source_packet_handler;
  hci_add_event_handler(&hci_event_callback_registration);

  hxcmod_initialized = hxcmod_init(&mod_context);
  if (hxcmod_initialized){
    hxcmod_setcfg(&mod_context, A2DP_SAMPLE_RATE, 16, 1, 1, 1);
    hxcmod_load(&mod_context, (void *) &mod_data, mod_len);
    printf("loaded mod '%s', size %u\n", mod_name, mod_len);
  }

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

hid_keyboard_demo: HID Keyboard (Server) Demo

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

Status: Basic implementation. HID Request from Host are not answered yet. Works with iOS.

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[]);
int btstack_main(int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  gap_discoverable_control(1);
  gap_set_class_of_device(0x2540);
  gap_set_local_name("HID Keyboard Demo 00:00:00:00:00:00");

  // L2CAP
  l2cap_init();

  // SDP Server
  sdp_init();
  memset(hid_service_buffer, 0, sizeof(hid_service_buffer));
  // hid sevice subclass 2540 Keyboard, hid counntry code 33 US, hid virtual cable off, hid reconnect initiate off, hid boot device off 
  hid_create_sdp_record(hid_service_buffer, 0x10001, 0x2540, 33, 0, 0, hid_boot_device, hid_descriptor_keyboard_boot_mode, sizeof(hid_descriptor_keyboard_boot_mode), hid_device_name);
  printf("HID service record size: %u\n", de_get_len( 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, 0x10003, DEVICE_ID_VENDOR_ID_SOURCE_BLUETOOTH, BLUETOOTH_COMPANY_ID_BLUEKITCHEN_GMBH, 1, 1);
  printf("Device ID SDP service record size: %u\n", de_get_len((uint8_t*)device_id_sdp_service_buffer));
  sdp_register_service(device_id_sdp_service_buffer);

  // HID Device
  hid_device_init(hid_boot_device);

  // 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  
  // turn on!
  hci_power_control(HCI_POWER_ON);
  return 0;
}

hid_mouse_demo: HID Mouse (Server) Demo

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

Status: Basic implementation. HID Request from Host are not answered yet. Works with iOS.

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[]);
int btstack_main(int argc, const char * argv[]){
  (void)argc;
  (void)argv;

  gap_discoverable_control(1);
  gap_set_class_of_device(0x2540);
  gap_set_local_name("HID Mouse Demo 00:00:00:00:00:00");

  // L2CAP
  l2cap_init();

  // SDP Server
  sdp_init();
  memset(hid_service_buffer, 0, sizeof(hid_service_buffer));
  // hid sevice subclass 2540 Keyboard, hid counntry code 33 US, hid virtual cable off, hid reconnect initiate off, hid boot device off
  hid_create_sdp_record(hid_service_buffer, 0x10001, 0x2540, 33, 0, 0, hid_boot_device, hid_descriptor_mouse_boot_mode, sizeof(hid_descriptor_mouse_boot_mode), hid_device_name);
  printf("SDP service record size: %u\n", de_get_len( hid_service_buffer));
  sdp_register_service(hid_service_buffer);

  // HID Device
  hid_device_init(hid_boot_device);

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

hog_keyboard_demo: HID-over-GATT Keyboard

hog_mouse_demo: HID-over-GATT Mouse

sm_pairing_central: LE Peripheral - Test pairing combinations

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 packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size);

static void sm_pairing_central_setup(void){
  l2cap_init();

  // setup le device db
  le_device_db_init();

  // setup SM: Display only
  sm_init();

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

  /**
   * Choose ONE of the following configurations

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

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

  att_server_register_packet_handler(packet_handler);

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

  // 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
  // LE Secure Connetions, 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 Legacy 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);
#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.

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;

  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){
        printf("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);
      uint8_t address_type = gap_event_advertising_report_get_address_type(packet);
      uint8_t length = gap_event_advertising_report_get_data_length(packet);
      const uint8_t * data = gap_event_advertising_report_get_data(packet);
      printf("Advertisement event: addr-type %u, addr %s, data[%u] ",
         address_type, bd_addr_to_str(address), length);
      printf_hexdump(data, length);
      if (!ad_data_contains_uuid16(length, (uint8_t *) data, REMOTE_SERVICE)) break;
      printf("Found remote with UUID %04x, connecting...\n", REMOTE_SERVICE);
      gap_stop_scan();
      gap_connect(address,address_type);
      break;
    }
    case SM_EVENT_JUST_WORKS_REQUEST:
      printf("Just works requested\n");
      sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
      break;
    case SM_EVENT_NUMERIC_COMPARISON_REQUEST:
      printf("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:
      printf("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:
          printf("Pairing complete, success\n");
          break;
        case ERROR_CODE_CONNECTION_TIMEOUT:
          printf("Pairing failed, timeout\n");
          break;
        case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
          printf("Pairing faileed, disconnected\n");
          break;
        case ERROR_CODE_AUTHENTICATION_FAILURE:
          printf("Pairing failed, reason = %u\n", sm_event_pairing_complete_get_reason(packet));
          break;
        default:
          break;
      }
      break;
    case HCI_EVENT_LE_META:
      // wait for connection complete
      if (hci_event_le_meta_get_subevent_code(packet) != HCI_SUBEVENT_LE_CONNECTION_COMPLETE) break;
      con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
      // start pairing
      sm_request_pairing(con_handle);
      break;
    case HCI_EVENT_ENCRYPTION_CHANGE: 
      con_handle = hci_event_encryption_change_get_connection_handle(packet);
      printf("Connection encrypted: %u\n", hci_event_encryption_change_get_encryption_enabled(packet));
      break;
    default:
      break;
  }
}

sm_pairing_peripheral: LE Peripheral - Test pairing combinations

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 and the device name. 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 hci_event_callback_registration;
static btstack_packet_callback_registration_t sm_event_callback_registration;
// static hci_con_handle_t con_handle;

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

const uint8_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', 
};
const uint8_t adv_data_len = sizeof(adv_data);

static void sm_peripheral_setup(void){

  l2cap_init();

  // setup le device db
  le_device_db_init();

  // setup SM: Display only
  sm_init();

  /**
   * Choose ONE of the following configurations

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

  // 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
  // LE Secure Connetions, 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_ONLY);
  sm_set_authentication_requirements(SM_AUTHREQ_SECURE_CONNECTION|SM_AUTHREQ_MITM_PROTECTION);

  // LE Legacy 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);
#endif

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

  // setup advertisements
  uint16_t adv_int_min = 0x0030;
  uint16_t adv_int_max = 0x0030;
  uint8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (uint8_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 SM events
  sm_event_callback_registration.callback = &packet_handler;
  sm_add_event_handler(&sm_event_callback_registration);

  // register for ATT 
  att_server_register_packet_handler(packet_handler);
}

Packet Handler

The packet handler is used to:

  • stop the counter after a disconnect
  • send a notification when the requested ATT_EVENT_CAN_SEND_NOW is received

static void packet_handler (uint8_t packet_type, uint16_t channel, uint8_t *packet, uint16_t size){
  UNUSED(channel);
  UNUSED(size);
  hci_con_handle_t con_handle;
  bd_addr_t addr;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        case HCI_EVENT_LE_META:
          switch (hci_event_le_meta_get_subevent_code(packet)) {
            case HCI_SUBEVENT_LE_CONNECTION_COMPLETE:
              // setup new 
              con_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
              sm_send_security_request(con_handle);
              break;
            default:
              break;
          }
          break;
        case SM_EVENT_JUST_WORKS_REQUEST:
          printf("Just Works requested\n");
          sm_just_works_confirm(sm_event_just_works_request_get_handle(packet));
          break;
        case SM_EVENT_NUMERIC_COMPARISON_REQUEST:
          printf("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:
          printf("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);
          printf("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);
          printf("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);
          printf("Identity resolving failed\n");
          break;
        case SM_EVENT_PAIRING_COMPLETE:
          switch (sm_event_pairing_complete_get_status(packet)){
            case ERROR_CODE_SUCCESS:
              printf("Pairing complete, success\n");
              break;
            case ERROR_CODE_CONNECTION_TIMEOUT:
              printf("Pairing failed, timeout\n");
              break;
            case ERROR_CODE_REMOTE_USER_TERMINATED_CONNECTION:
              printf("Pairing faileed, disconnected\n");
              break;
            case ERROR_CODE_AUTHENTICATION_FAILURE:
              printf("Pairing failed, reason = %u\n", sm_event_pairing_complete_get_reason(packet));
              break;
            default:
              break;
          }
          break;
        default:
          break;
    }
    break;
  }
}

pbap_client_demo: Connect to Phonebook Server and get contacts.

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.

le_streamer: LE Streamer - Stream data over GATT.

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

  • send whenever possible,
  • use the max ATT MTU.

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 le device db
  le_device_db_init();

  // setup SM: Display only
  sm_init();

  // 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
  att_server_register_packet_handler(att_packet_handler);

  // setup advertisements
  uint16_t adv_int_min = 0x0030;
  uint16_t adv_int_max = 0x0030;
  uint8_t adv_type = 0;
  bd_addr_t null_addr;
  memset(null_addr, 0, 6);
  gap_advertisements_set_params(adv_int_min, adv_int_max, adv_type, 0, null_addr, 0x07, 0x00);
  gap_advertisements_set_data(adv_data_len, (uint8_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, int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  uint32_t now = btstack_run_loop_get_time_ms();
  uint32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // print speed
  int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  printf("%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);

  uint16_t conn_interval;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      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) {
            printf("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:
          context = connection_for_conn_handle(hci_event_disconnection_complete_get_connection_handle(packet));
          if (!context) break;
          // free connection
          printf("%c: Disconnect, reason %02x\n", context->name, hci_event_disconnection_complete_get_reason(packet));          
          context->le_notification_enabled = 0;
          context->connection_handle = HCI_CON_HANDLE_INVALID;
          break;
        case HCI_EVENT_LE_META:
          switch (hci_event_le_meta_get_subevent_code(packet)) {
            case HCI_SUBEVENT_LE_CONNECTION_COMPLETE:
              // setup new 
              context = connection_for_conn_handle(HCI_CON_HANDLE_INVALID);
              if (!context) break;
              context->counter = 'A';
              context->test_data_len = ATT_DEFAULT_MTU - 3;
              context->connection_handle = hci_subevent_le_connection_complete_get_connection_handle(packet);
              // print connection parameters (without using float operations)
              conn_interval = hci_subevent_le_connection_complete_get_conn_interval(packet);
              printf("%c: Connection Interval: %u.%02u ms, latency %u\n", context->name, conn_interval * 125 / 100,
                25 * (conn_interval & 3), hci_subevent_le_connection_update_complete_get_conn_latency(packet));

              // request min con interval 15 ms for iOS 11+ 
              // gap_request_connection_parameter_update(context->connection_handle, 12, 12, 0, 0x0048);
              break;
            case HCI_SUBEVENT_LE_CONNECTION_UPDATE_COMPLETE:
              // print connection parameters (without using float operations)
              context = connection_for_conn_handle(hci_subevent_le_connection_update_complete_get_connection_handle(packet));
              if (!context) break;
              conn_interval = hci_subevent_le_connection_update_complete_get_conn_interval(packet);
              printf("%c: Connection Interval: %u.%02u ms, latency %u\n", context->name, conn_interval * 125 / 100,
                25 * (conn_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 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);

  int mtu;
  le_streamer_connection_t * context;
  switch (packet_type) {
    case HCI_EVENT_PACKET:
      switch (hci_event_packet_get_type(packet)) {
        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));
          printf("%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;
        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
  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
  att_server_notify(context->connection_handle, context->value_handle, (uint8_t*) context->test_data, context->test_data_len);

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

  // request next send event
  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);

  // printf("att_write_callback att_handle %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;
      printf("%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;
        }
        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:
      printf("Write to 0x%04x, len %u\n", att_handle, buffer_size);
  }
  return 0;
}

le_streamer_client: 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
typedef struct {
  char name;
  int le_notification_enabled;
  int  counter;
  char test_data[200];
  int  test_data_len;
  uint32_t test_data_sent;
  uint32_t test_data_start;
} le_streamer_connection_t;

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, int bytes_sent){
  context->test_data_sent += bytes_sent;
  // evaluate
  uint32_t now = btstack_run_loop_get_time_ms();
  uint32_t time_passed = now - context->test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // print speed
  int bytes_per_second = context->test_data_sent * 1000 / time_passed;
  printf("%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;
}

spp_streamer: Send test data via SPP as fast as possible.

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 uint32_t test_data_transferred;
static uint32_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(int bytes_sent){
  test_data_transferred += bytes_sent;
  // evaluate
  uint32_t now = btstack_run_loop_get_time_ms();
  uint32_t time_passed = now - test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // print speed
  int bytes_per_second = test_data_transferred * 1000 / time_passed;
  printf("%u bytes -> %u.%03u kB/s\n", (int) test_data_transferred, (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();

  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, 0x10001, RFCOMM_SERVER_CHANNEL, "SPP Streamer");
  sdp_register_service(spp_service_buffer);
  // printf("SDP service record size: %u\n", de_get_len(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;
}

spp_streamer_client: Client for SPP Streamer

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 uint32_t test_data_transferred;
static uint32_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(int bytes_sent){
  test_data_transferred += bytes_sent;
  // evaluate
  uint32_t now = btstack_run_loop_get_time_ms();
  uint32_t time_passed = now - test_data_start;
  if (time_passed < REPORT_INTERVAL_MS) return;
  // print speed
  int bytes_per_second = test_data_transferred * 1000 / time_passed;
  printf("%u bytes -> %u.%03u kB/s\n", (int) test_data_transferred, (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[]);
int btstack_main(int argc, const char * argv[]){
  UNUSED(argc);
  (void)argv;

  l2cap_init();

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

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

dut_mode_classic: Enable Device Under Test (DUT) Mode for BR/EDR

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[]);
int btstack_main(int argc, const char * argv[]) {
  (void)argc;
  (void)argv;

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